Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

did:web resolution #13

Merged
merged 4 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.1.2"),
.package(url: "https://github.com/Frizlab/swift-typeid.git", from: "0.3.0"),
.package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"),
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "3.0.1")),
],
targets: [
// Main tbDEX library target
Expand Down Expand Up @@ -52,11 +53,13 @@ let package = Package(
"tbDEX",
"TestUtilities",
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Mocker", package: "Mocker"),
],
resources: [
.copy("Resources/ed25519"),
.copy("Resources/secp256k1"),
.copy("Resources/did_jwk"),
.copy("Resources/did_web"),
]
),
]
Expand Down
14 changes: 12 additions & 2 deletions Sources/tbDEX/Dids/DidResolution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import Foundation

enum DidResolution {

/// Errors that can occur during DID resolution process
enum Error: String {
case invalidDid
case methodNotSupported
case notFound
}

/// Representation of the result of a DID (Decentralized Identifier) resolution
///
/// [Specification Reference](https://www.w3.org/TR/did-core/#resolution)
Expand Down Expand Up @@ -37,9 +44,12 @@ enum DidResolution {
self.didDocumentMetadata = didDocumentMetadata
}

static func invalidDid() -> Result {
/// Convenience function to create a `DidResolution.Result` with an error
/// - Parameter error: Specific error which caused DID to not resolve
/// - Returns: DidResolution.Result with appropriate error metadata
static func resolutionError(_ error: DidResolution.Error) -> Result {
Result(
didResolutionMetadata: Metadata(error: "invalidDid"),
didResolutionMetadata: Metadata(error: error.rawValue),
didDocument: nil,
didDocumentMetadata: DidDocument.Metadata()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,14 @@ struct DidJwk: Did {
/// - Parameter didUri: The DID URI to resolve
/// - Returns: `DidResolution.Result` containing the resolved DID Document.
static func resolve(didUri: String) -> DidResolution.Result {
let parsedDid: ParsedDid
do {
parsedDid = try ParsedDid(uri: didUri)
} catch {
return DidResolution.Result.invalidDid()
guard let parsedDid = try? ParsedDid(didUri: didUri),
let jwk = try? JSONDecoder().decode(Jwk.self, from: try parsedDid.methodSpecificId.decodeBase64Url())
else {
return DidResolution.Result.resolutionError(.invalidDid)
}

guard parsedDid.method == "jwk" else {
return DidResolution.Result.invalidDid()
}

let jwk: Jwk

do {
jwk = try JSONDecoder().decode(Jwk.self, from: try parsedDid.id.decodeBase64Url())
} catch {
return DidResolution.Result.invalidDid()
guard parsedDid.methodName == "jwk" else {
return DidResolution.Result.resolutionError(.methodNotSupported)
}

let verifiationMethod = DidVerificationMethod(
Expand Down
51 changes: 51 additions & 0 deletions Sources/tbDEX/Dids/Methods/Web/DidWeb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

struct DidWeb {

// MARK: - Public Static

/// Resolves a `did:jwk` URI into a `DidResolution.Result`
/// - Parameter didUri: The DID URI to resolve
/// - Returns: `DidResolution.Result` containing the resolved DID Document.
static func resolve(didUri: String) async -> DidResolution.Result {
guard let parsedDid = try? ParsedDid(didUri: didUri),
let url = getDidDocumentUrl(methodSpecificId: parsedDid.methodSpecificId)
else {
return DidResolution.Result.resolutionError(.invalidDid)
}

guard parsedDid.methodName == "web" else {
return DidResolution.Result.resolutionError(.methodNotSupported)
}

do {
let response = try await URLSession.shared.data(from: url)
let didDocument = try JSONDecoder().decode(DidDocument.self, from: response.0)
return DidResolution.Result(didDocument: didDocument)
} catch {
return DidResolution.Result.resolutionError(.notFound)
}
}

// MARK: - Private Static

private static let wellKnownPath = "/.well-known"
private static let didDocumentFilename = "/did.json"

private static func getDidDocumentUrl(methodSpecificId: String) -> URL? {
let domainNameWithPath = methodSpecificId.replacingOccurrences(of: ":", with: "/")
guard let decodedDomain = domainNameWithPath.removingPercentEncoding,
var url = URL(string: "https://\(decodedDomain)")
else {
return nil
}

if url.path.isEmpty {
url.appendPathComponent(wellKnownPath)
}

url.appendPathComponent(didDocumentFilename)
return url
}

}
56 changes: 38 additions & 18 deletions Sources/tbDEX/Dids/ParsedDid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,63 @@ import Foundation

enum ParsedDidError: Error {
case invalidUri
case invalidMethodName
case invalidMethodSpecificId
}

/// Parsed Decentralized Identifier (DID) URI, according to the specifications
/// Parsed Decentralized Identifier (DID), according to the specifications
/// defined by the [W3C DID Core specification](https://www.w3.org/TR/did-core).
struct ParsedDid {

/// The complete DID URI.
private(set) var uri: String

/// The method specified in the DID URI.
/// The method name specified in the DID URI.
///
/// Example: if the `uri` is `did:example:123456`, "example" would be the method name
private(set) var method: String
private(set) var methodName: String

/// The identifier part of the DID URI.
/// The method specific identifier part of the DID URI.
///
/// Example: if the `uri` is `did:example:123456`, "123456" would be the identifier
private(set) var id: String

/// Regex pattern for parsing DID URIs.
static let didUriPattern = #"did:([a-z0-9]+):([a-zA-Z0-9._%-]+(?:\:[a-zA-Z0-9._%-]+)*)"#
private(set) var methodSpecificId: String

/// Parses a DID URI in accordance to the ABNF rules specified in the specification
/// [here](https://www.w3.org/TR/did-core/#did-syntax).
/// - Parameter input: URI of DID to parse
/// - Returns: `DidUri` instance if parsing was successful. Throws error otherwise.
init(uri: String) throws {
let regex = try NSRegularExpression(pattern: Self.didUriPattern)
guard let match = regex.firstMatch(in: uri, range: NSRange(uri.startIndex..., in: uri)) else {
/// - Parameter didUri: URI of DID to parse
/// - Returns: `ParsedDid` instance if parsing was successful. Throws error otherwise.
init(didUri: String) throws {
let components = didUri.components(separatedBy: ":")

guard components.count >= 3 else {
throw ParsedDidError.invalidUri
}

let methodRange = Range(match.range(at: 1), in: uri)!
let methodSpecificIdRange = Range(match.range(at: 2), in: uri)!
let methodName = components[1]
guard Self.isValidMethodName(methodName) else {
throw ParsedDidError.invalidMethodName
}

let methodSpecificId = components.dropFirst(2).joined(separator: ":")
guard Self.isValidMethodSpecificId(methodSpecificId) else {
throw ParsedDidError.invalidMethodSpecificId
}

self.uri = didUri
self.methodName = methodName
self.methodSpecificId = methodSpecificId
}

// MARK: - Private Static

private static let methodNameRegex = "^[a-z0-9]+$"
private static let methodSpecificIdRegex = "^(([a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+|%[0-9a-fA-F]{2})+$"

private static func isValidMethodName(_ methodName: String) -> Bool {
return methodName.range(of: methodNameRegex, options: .regularExpression) != nil
}

self.uri = uri
self.method = String(uri[methodRange])
self.id = String(uri[methodSpecificIdRange])
private static func isValidMethodSpecificId(_ id: String) -> Bool {
return id.range(of: methodSpecificIdRegex, options: .regularExpression) != nil
}
}
98 changes: 98 additions & 0 deletions Tests/Web5TestVectors/Resources/did_web/resolve.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"description": "did:web resolution",
"vectors": [
{
"description": "resolves to a well known URL",
"input": {
"didUri": "did:web:example.com",
"mockServer": {
"https://example.com/.well-known/did.json": {
"id": "did:web:example.com"
}
}
},
"output": {
"didResolutionMetadata": {},
"didDocument": {
"id": "did:web:example.com"
},
"didDocumentMetadata": {}
}
},
{
"description": "resolves to a URL with a path",
"input": {
"didUri": "did:web:w3c-ccg.github.io:user:alice",
"mockServer": {
"https://w3c-ccg.github.io/user/alice/did.json": {
"id": "did:web:w3c-ccg.github.io:user:alice"
}
}
},
"output": {
"didResolutionMetadata": {},
"didDocument": {
"id": "did:web:w3c-ccg.github.io:user:alice"
},
"didDocumentMetadata": {}
}
},
{
"description": "resolves to a URL with a path and a port",
"input": {
"didUri": "did:web:example.com%3A3000:user:alice",
"mockServer": {
"https://example.com:3000/user/alice/did.json": {
"id": "did:web:example.com%3A3000:user:alice"
}
}
},
"output": {
"didResolutionMetadata": {},
"didDocument": {
"id": "did:web:example.com%3A3000:user:alice"
},
"didDocumentMetadata": {}
}
},
{
"description": "methodNotSupported error returned when did method is not web",
"input": {
"didUri": "did:dht:gb46emk73wkenrut43ii67a3o5qctojcaucebth7r83pst6yeh8o"
},
"output": {
"didResolutionMetadata": {
"error": "methodNotSupported"
},
"didDocumentMetadata": {}
},
"errors": true
},
{
"description": "notFound error returned when domain does not exist",
"input": {
"didUri": "did:web:doesnotexist.com"
},
"output": {
"didResolutionMetadata": {
"error": "notFound"
},
"didDocumentMetadata": {}
},
"errors": true
},
{
"description": "invalidDid error returned for domain name with invalid character",
"input": {
"didUri": "did:web:invalidcharø.com"
},
"output": {
"didResolutionMetadata": {
"error": "invalidDid"
},
"didDocumentMetadata": {}
},
"errors": true
}
]
}
53 changes: 53 additions & 0 deletions Tests/Web5TestVectors/Web5TestVectorsDidWeb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import CustomDump
import Mocker
import TestUtilities
import XCTest

@testable import tbDEX

final class Web5TestVectorsDidWeb: XCTestCase {

func test_resolve() throws {
struct Input: Codable {
let didUri: String
let mockServer: [String: [String: String]]?

func mocks() throws -> [Mock] {
guard let mockServer = mockServer else { return [] }

return try mockServer.map({ key, value in
print("Mocking \(key)")
return Mock(
url: URL(string: key)!,
dataType: .json,
statusCode: 200,
data: [
.get: try JSONEncoder().encode(value)
]
)
})
}
}

let testVector = try TestVector<Input, DidResolution.Result>(
fileName: "resolve",
subdirectory: "did_web"
)

testVector.run { vector in
let expectation = XCTestExpectation(description: "async resolve")
Task {
/// Register each of the mock network responses
try vector.input.mocks().forEach { $0.register() }

/// Resolve each input didUri, make sure it matches output
let result = await DidWeb.resolve(didUri: vector.input.didUri)
XCTAssertNoDifference(result, vector.output)
expectation.fulfill()
}

wait(for: [expectation], timeout: 1)
}
}

}
Loading