generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 2
/
DIDDHT.swift
455 lines (383 loc) · 17.9 KB
/
DIDDHT.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
import DNS
import Foundation
/// `did:dht` DID Method
public enum DIDDHT: DIDMethod {
public static let methodName = "dht"
}
// MARK: - DIDMethodResolver
extension DIDDHT {
/// Resolver for the `did:dht` DID method
public struct Resolver: DIDMethodResolver {
// MARK: Types
/// Options that can be configured for resolving `did:dht` DIDs
public struct ResolutionOptions {
/// The URI of a server involved in executing DID method operations. In the context of
/// DID creation, the endpoint is expected to be a Pkarr relay.
let gatewayURI: String
/// Public Memberwise Initializer
public init(
gatewayURI: String
) {
self.gatewayURI = gatewayURI
}
/// Default `ResolutionOptions`
public static let `default` = ResolutionOptions(
gatewayURI: "https://diddht.tbddev.org"
)
}
// MARK: Properties
public let methodName = DIDDHT.methodName
/// The options to use for the resolution process
public let options: ResolutionOptions
// MARK: Lifecycle
/// Initialize a new resolver for the `did:dht` method
/// - Parameters:
/// - options: The options to use for the resolution process
public init(
options: ResolutionOptions = .default
) {
self.options = options
}
// MARK: Public Functions
/// Resolves a `did:dht` URI into a `DIDResolutionResult`
/// - Parameters:
/// - didURI: The DID URI to resolve
/// - Returns: `DIDResolution.Result` containing the resolved DID Document.
public func resolve(
didURI: String
) async -> DIDResolutionResult {
guard let did = try? DID(didURI: didURI) else {
return DIDResolutionResult(error: .invalidDID)
}
guard did.methodName == methodName else {
return DIDResolutionResult(error: .methodNotSupported)
}
return await Document.resolve(did: did, gatewayURI: options.gatewayURI)
}
}
}
// MARK: - DIDDHT.Document
extension DIDDHT {
/// `DIDDHT.Document` provides functionality for interacting with the DID document stored in
/// Mainline DHT in support of DID DHT method create, resolve, update, and deactivate operations.
///
/// This class includes methods for retrieving and publishing DID documents to and from the DHT,
/// using DNS packet encoding and Pkarr relay servers.
enum Document {
/// Retrives a DID document and its metadata from the DHT network
/// - Parameters:
/// - did: The DID whose document to retrieve
/// - gatewayURI: The DID DHT Gateway or Pkarr Relay URI
/// - Returns: DIDResolutionResult containing the DID document and its metadata
static func resolve(
did: DID,
gatewayURI: String
) async -> DIDResolutionResult {
guard let identityKeyBytes = try? ZBase32.decode(did.identifier) else {
return DIDResolutionResult(error: .invalidPublicKey)
}
if identityKeyBytes.count != 32 {
return DIDResolutionResult(error: .invalidPublicKeyLength)
}
guard let bep44Message = try? await DIDDHT.Document.pkarrGet(
publicKeyBytes: identityKeyBytes,
gatewayURI: gatewayURI
) else {
return DIDResolutionResult(error: .notFound)
}
do {
let dnsPacket = try parseBEP44GetMessage(bep44Message)
var (didDocument, didDocumentMetadata) = try fromDNSPacket(did: did, dnsPacket: dnsPacket)
didDocumentMetadata.versionId = String(bep44Message.seq)
return DIDResolutionResult(
didDocument: didDocument,
didDocumentMetadata: didDocumentMetadata
)
} catch Error.resolutionError(let resolutionError) {
// A specific resolution error occured.
return DIDResolutionResult(error: resolutionError)
} catch {
// Some other error happened, treat it as an internal error
return DIDResolutionResult(error: .internalError)
}
}
/// Retrieves a signed BEP44Message from a DID DHT Gateway or Pkarr Relay server
/// - Parameters:
/// - publicKeyBytes: The public key bytes of the identity key, zbase32 encoded
/// - gatewayURI: The DID DHT Gateway or Pkarr Relay URI
/// - Returns: BEP44Message containing the signed DNS packet
static func pkarrGet(
publicKeyBytes: Data,
gatewayURI: String
) async throws -> BEP44Message {
let identifier = ZBase32.encode(publicKeyBytes)
guard let baseURL = URL(string: gatewayURI),
let relativeURL = URL(string: identifier, relativeTo: baseURL)
else {
throw Error.resolutionError(.notFound)
}
let (data, response) = try await URLSession.shared.data(from: relativeURL)
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode)
else {
throw Error.resolutionError(.notFound)
}
if data.count < 72 {
throw Error.resolutionError(.invalidDIDDocumentLength)
}
if data.count > 1072 {
throw Error.resolutionError(.invalidDIDDocumentLength)
}
let seqData = data.subdata(in: 64..<72)
let seq: Int64 = seqData.withUnsafeBytes { $0.load(as: Int64.self) }.bigEndian
return BEP44Message(
k: publicKeyBytes,
seq: seq,
sig: data.prefix(64),
v: data.suffix(from: 72)
)
}
/// Parses and verifies a BEP44 Get Message, converting it to a DNS packet
/// - Parameters:
/// - message: BEP44Message to verify and parse
/// - Returns: DNS packet represented by the BEP44Message
static func parseBEP44GetMessage(_ message: BEP44Message) throws -> DNS.Message {
let publicKey = try Ed25519.publicKeyFromBytes(message.k)
let bencodedData = try (
Bencode.encodeAsBytes("seq") +
Bencode.encodeAsBytes(message.seq) +
Bencode.encodeAsBytes("v") +
Bencode.encodeAsBytes(message.v)
)
let isValid = try Ed25519.verify(
payload: bencodedData,
signature: message.sig,
publicKey: publicKey
)
if !isValid {
throw Error.resolutionError(.invalidSignature)
}
var message = try DNS.Message(deserialize: message.v)
// DNS.Message(deserialize:) doesn't parse out each of the answer's TextRecord attributes,
// and instead assumes there's only ever one value. Detect this, and parse out any other
// attributes that may be separated by semicolons.
message.answers = message.answers.map { answer in
if var answer = answer as? TextRecord {
for (k, v) in answer.attributes {
let splitValue = v.components(separatedBy: DIDDHT.Constants.PROPERTY_SEPARATOR)
answer.attributes[k] = splitValue[0]
for i in 1..<splitValue.count {
let parts = splitValue[i].components(separatedBy: "=")
if parts.count == 2 {
answer.attributes[parts[0]] = parts[1]
}
}
}
return answer
} else {
return answer
}
}
return message
}
/// Converts a DNS packet into a DID Document and its associated metadata,
/// according to the `did:dht` [spec](https://tbd54566975.github.io/did-dht-method/#dids-as-a-dns-packet)
static func fromDNSPacket(
did: DID,
dnsPacket: DNS.Message
) throws -> (DIDDocument, DIDDocument.Metadata) {
var idLookup = [String: String]()
var alsoKnownAs: [String]?
var controllers: [String]?
var verificationMethods: [VerificationMethod]?
var services: [Service]?
var authentication: [EmbeddedOrReferencedVerificationMethod]?
var assertionMethod: [EmbeddedOrReferencedVerificationMethod]?
var capabilityDelegation: [EmbeddedOrReferencedVerificationMethod]?
var capabilityInvocation: [EmbeddedOrReferencedVerificationMethod]?
var keyAgreement: [EmbeddedOrReferencedVerificationMethod]?
var types: [Int]?
var rootRecord: TextRecord? = nil
/// `did:dht` properties are ONLY present in DNS TXT records.
/// Loop through the answers, only taking into consideration the text records.
for case let answer as TextRecord in dnsPacket.answers {
let dnsRecordID = String(answer.name.prefix { $0 != "." }.dropFirst())
if dnsRecordID.hasPrefix("aka") {
// Process an also known as record
alsoKnownAs = answer.values
} else if dnsRecordID.hasPrefix("cnt") {
// Process a controller record
controllers = answer.values
} else if dnsRecordID.hasPrefix("k") {
// Process verification methods
let publicKeyBytes = try answer.attributes["k"]!.decodeBase64Url()
let namedCurve = RegisteredKeyType(rawValue: Int(answer.attributes["t"]!)!)
var publicKey: Jwk
switch namedCurve {
case .Ed25519:
publicKey = try Ed25519.publicKeyFromBytes(publicKeyBytes)
case .secp256k1:
publicKey = try Secp256k1.publicKeyFromBytes(publicKeyBytes)
default:
throw Error.resolutionError(.unsupportedPublicKey)
}
// If this is the `k0` record, this key represents the identity key.
// Always set this `kid` to `"0"` for the identity key.
if dnsRecordID == "k0" {
publicKey.keyID = "0"
}
if verificationMethods == nil {
verificationMethods = []
}
let methodID = "\(did.uri)#\(answer.attributes["id"]!)"
verificationMethods?.append(
VerificationMethod(
id: methodID,
type: "JsonWebKey",
controller: answer.attributes["c"] ?? did.uri,
publicKeyJwk: publicKey
)
)
idLookup[dnsRecordID] = methodID
} else if dnsRecordID.hasPrefix("s") {
// Process services
let id = "\(did.uri)#\(answer.attributes["id"]!)"
let serviceEndpoint = answer.attributes["se"]!
let type = answer.attributes["t"]!
if services == nil {
services = []
}
services?.append(
Service(
id: id,
type: type,
serviceEndpoint: OneOrMany(serviceEndpoint.components(separatedBy: Constants.VALUE_SEPARATOR))!
)
)
} else if dnsRecordID.hasPrefix("typ") {
// Process DID DHT types
guard let values = answer.attributes["id"] else {
fatalError("types not found")
}
types = values.components(separatedBy: Constants.VALUE_SEPARATOR).compactMap { Int($0) }
} else if dnsRecordID.hasPrefix("did") {
// Save the root record for processing after all other records
rootRecord = answer
}
}
// Parse root record last (if present).
// This is done last, as it depends on other DNSPacket records.
if let rootRecord {
func recordIDsToMethodIDs(data: String) -> [String] {
return data
.components(separatedBy: Constants.VALUE_SEPARATOR)
.compactMap { idLookup[String($0)] }
}
if let auth = rootRecord.attributes["auth"] {
authentication = recordIDsToMethodIDs(data: auth).map { .referenced($0) }
}
if let asm = rootRecord.attributes["asm"] {
assertionMethod = recordIDsToMethodIDs(data: asm).map { .referenced($0) }
}
if let del = rootRecord.attributes["del"] {
capabilityDelegation = recordIDsToMethodIDs(data: del).map{ .referenced($0) }
}
if let inv = rootRecord.attributes["inv"] {
capabilityInvocation = recordIDsToMethodIDs(data: inv).map { .referenced($0) }
}
if let agm = rootRecord.attributes["agm"] {
keyAgreement = recordIDsToMethodIDs(data: agm).map { .referenced($0) }
}
}
return (
DIDDocument(
id: did.uri,
alsoKnownAs: alsoKnownAs,
controller: OneOrMany(controllers),
verificationMethod: verificationMethods,
service: services,
assertionMethod: assertionMethod,
authentication: authentication,
keyAgreement: keyAgreement,
capabilityDelegation: capabilityDelegation,
capabilityInvocation: capabilityInvocation
),
DIDDocument.Metadata(
types: types
)
)
}
}
/// Enumeration of the the types of keys that can be used in a DID DHT document.
///
/// The DID DHT method supports various cryptographic key types. These key types are essential for
/// the creation and management of DIDs and their associated cryptographic operations like signing
/// and encryption. The registered key types are published in the DID DHT Registry and each is
/// assigned a unique numerical value for use by client and gateway implementations.
///
/// The registered key types are published in the
/// [DID DHT Registry](https://did-dht.com/registry/index.html#key-type-index).
enum RegisteredKeyType: Int {
/// A public-key signature system using the EdDSA (Edwards-curve Digital Signature Algorithm) and Curve25519.
case Ed25519 = 0
/// A cryptographic curve used for digital signatures in a range of decentralized systems.
case secp256k1 = 1
/// Also known as P-256 or prime256v1, this curve is used for cryptographic operations and is widely
/// supported in various cryptographic libraries and standards.
case secp256r1 = 2
}
enum Constants {
/// Character used to separate distinct elements or entries in the DNS packet representation of a DID Document.
///
/// For example, verification methods, verification relationships, and services are separated by
/// semicolons (`;`) in the root record:
/// ```
/// vm=k1;auth=k1;asm=k2;inv=k3;del=k3;srv=s1
/// ```
static let PROPERTY_SEPARATOR = ";"
/// Character used to separate distinct values within a single element or entry in the DNS packet
/// representation of a DID Document.
///
/// For example, multiple key references for the `authentication` verification relationships are
/// separated by commas (`,`):
/// ```
/// auth=0,1,2
/// ```
static let VALUE_SEPARATOR = ","
}
}
// MARK: - BEP44Message
/// Represents a BEP44 message, which is used for storing and retrieving data in the Mainline DHT
/// network.
///
/// A BEP44 message is used primarily in the context of the DID DHT method for publishing and
/// resolving DID documents in the DHT network. This type encapsulates the data structure required
/// for such operations in accordance with BEP44.
///
/// See [BEP44](https://www.bittorrent.org/beps/bep_0044.html) for more information.
struct BEP44Message {
/// The public key bytes of the Identity Key, which serves as the identifier in the DHT network for
/// the corresponding BEP44 message.
let k: Data
/// The sequence number of the message, used to ensure the latest version of the data is retrieved
/// and updated. It's a monotonically increasing number.
let seq: Int64
/// The signature of the message, ensuring the authenticity and integrity of the data. It's
/// computed over the bencoded sequence number and value.
let sig: Data
/// The actual data being stored or retrieved from the DHT network, typically encoded in a format
/// suitable for DNS packet representation of a DID Document.
let v: Data
}
// MARK: - Errors
extension DIDDHT {
enum Error: LocalizedError {
case resolutionError(DIDResolutionResult.Error)
public var errorDescription: String? {
switch self {
case let .resolutionError(error):
return "Error resolving DID: \(error)"
}
}
}
}