Skip to content

Commit

Permalink
PortableDID refactoring part 1 (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
amika-sq authored Feb 6, 2024
1 parent 83a00c2 commit f24e37b
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 70 deletions.
9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ let package = Package(
.package(url: "https://github.com/swift-extras/swift-extras-base64.git", from: "0.7.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.1.2"),
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "3.0.1")),
.package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0")
.package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"),
.package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"),
],
targets: [
.target(
name: "Web5",
dependencies: [
.product(name: "secp256k1", package: "secp256k1.swift"),
.product(name: "ExtrasBase64", package: "swift-extras-base64"),
.product(name: "AnyCodable", package: "anycodable"),
]
),
.testTarget(
name: "Web5Tests",
dependencies: ["Web5"]
dependencies: [
"Web5",
.product(name: "CustomDump", package: "swift-custom-dump"),
]
),
.testTarget(
name: "Web5TestVectors",
Expand Down
85 changes: 43 additions & 42 deletions Sources/Web5/Dids/BearerDID.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AnyCodable
import Foundation

// BearerDID is a composite type that combines a DID with a KeyManager containing keys
Expand All @@ -6,35 +7,37 @@ import Foundation
@dynamicMemberLookup
public struct BearerDID {

/// The DID object
private let did: DID
public typealias Metadata = [String: AnyCodable]

/// The `DID` object represented by this `BearerDID`
public let did: DID

/// The DIDDocument associated with this `BearerDID`
public let document: DIDDocument

/// The `KeyManager` which manages the keys for this DID
public let keyManager: KeyManager

/// Method-specific data associated with this `BearerDID`
public let metadata: Metadata?

/// Default initializer
public init(
didURI: String,
keyManager: KeyManager
///
/// - Parameters:
/// - did: `DID` to create the `BearerDID` from
/// - document: `DIDDocument` associated with the provided `did`
/// - keyManager: `KeyManager` where the private key material for the provided `did` are stored
/// - metadata: Additional method-specific metadata to be included with the `BearerDID`
init(
did: DID,
document: DIDDocument,
keyManager: KeyManager,
metadata: Metadata? = nil
) throws {
self.did = try DID(didURI: didURI)
self.did = did
self.document = document
self.keyManager = keyManager
}

/// Construct a `BearerDID` from a `PortableDID`, storing the keys in a
/// bespoke `InMemoryKeyManager` instance
init(portableDID: PortableDID) throws {
let did = try DID(didURI: portableDID.uri)

let keyManager = InMemoryKeyManager()
for verificationMethodPair in portableDID.verificationMethods {
_ = try keyManager.import(key: verificationMethodPair.privateKey)
}

try self.init(
didURI: did.uri,
keyManager: keyManager
)
self.metadata = metadata
}

/// @dynamicMemberLookup allows us to access properties of the DID directly
Expand All @@ -44,37 +47,32 @@ public struct BearerDID {

/// Exports the `BearerDID` into a portable format that contains the DID's URI in addition
/// to every private key associated with a verifification method.
public func toPortableDID() async throws -> PortableDID {
public func export() throws -> PortableDID {
guard let exporter = keyManager as? KeyExporter else {
throw BearerDID.Error.keyManagerNotExporter(keyManager)
}

let resolutionResult = await DIDResolver.resolve(didURI: did.uri)
if let error = resolutionResult.didResolutionMetadata.error {
throw BearerDID.Error.didResolutionError(error)
throw Error.keyManagerNotExporter(keyManager)
}

let verificationMethods: [PortableDID.VerificationMethodKeyPair] =
resolutionResult
.didDocument?
let privateKeys: [Jwk] =
try document
.verificationMethod?
.compactMap { verificationMethod in
.map { verificationMethod in
guard let publicKey = verificationMethod.publicKeyJwk,
let keyAlias = try? keyManager.getDeterministicAlias(key: publicKey),
let privateKey = try? exporter.exportKey(keyAlias: keyAlias)
else {
return nil
throw Error.exportError(
"Failed to export privateKey for verificationMethod \(verificationMethod.id)"
)
}

return PortableDID.VerificationMethodKeyPair(
publicKey: publicKey,
privateKey: privateKey
)
return privateKey
} ?? []

return PortableDID(
uri: did.uri,
verificationMethods: verificationMethods
document: document,
privateKeys: privateKeys,
metadata: metadata
)
}
}
Expand All @@ -85,14 +83,17 @@ extension BearerDID {

public enum Error: LocalizedError {
case keyManagerNotExporter(KeyManager)
case didResolutionError(String)
case keyManagerNotImporter(KeyManager)
case exportError(String)

public var errorDescription: String? {
switch self {
case let .keyManagerNotExporter(keyManager):
return "\(String(describing: type(of: keyManager))) does not support exporting keys"
case let .didResolutionError(error):
return "Failed to resolve DID: \(error)"
case let .keyManagerNotImporter(keyManager):
return "\(String(describing: type(of: keyManager))) does not support importing keys"
case let .exportError(error):
return "Export error: \(error)"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Web5/Dids/DID.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Decentralized Identifier (DID), according to the [W3C DID Core specification](https://www.w3.org/TR/did-core).
public struct DID {
public struct DID: Equatable {

/// Represents the complete Decentralized Identifier (DID) URI
///
Expand Down
70 changes: 66 additions & 4 deletions Sources/Web5/Dids/Methods/DIDJWK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,48 @@ public enum DIDJWK {
let keyAlias = try keyManager.generatePrivateKey(algorithm: options.algorithm)
let publicKey = try keyManager.getPublicKey(keyAlias: keyAlias)
let publicKeyBase64Url = try JSONEncoder().encode(publicKey).base64UrlEncodedString()

let didURI = "did:jwk:\(publicKeyBase64Url)"
let did = try DID(didURI: didURI)
let document = Self.didDocument(did: did, publicKey: publicKey)

return try BearerDID(didURI: didURI, keyManager: keyManager)
return try BearerDID(
did: did,
document: document,
keyManager: keyManager
)
}

/// Import a `PortableDID` that represents a DIDJWK into a `BearerDID` that can be used
/// to sign and verify data
///
/// - Parameters:
/// - portableDID: The `PortableDID` to import
/// - keyManager: `KeyManager` to place the imported private keys. Defaults to `InMemoryKeyManager`
public static func `import`(
portableDID: PortableDID,
keyManager: KeyManager = InMemoryKeyManager()
) throws -> BearerDID {
let did = try DID(didURI: portableDID.uri)
guard did.methodName == methodName else {
throw Error.importError(
"Expected PortableDID with DID method \(methodName), was provided \(did.methodName)")
}

guard let importer = keyManager as? KeyImporter else {
throw Error.importError("KeyManager does not support importing keys")
}

// Import the privateKeys into the keyManager
for privateKey in portableDID.privateKeys {
_ = try importer.import(key: privateKey)
}

return try BearerDID(
did: did,
document: portableDID.document,
keyManager: keyManager
)
}

/// Resolves a `did:jwk` URI into a `DIDResolutionResult`
Expand All @@ -53,14 +92,24 @@ public enum DIDJWK {
return DIDResolutionResult(error: .methodNotSupported)
}

let didDocument = didDocument(did: did, publicKey: jwk)
return DIDResolutionResult(didDocument: didDocument)
}

// MARK: - Private

private static func didDocument(
did: DID,
publicKey: Jwk
) -> DIDDocument {
let verifiationMethod = VerificationMethod(
id: "\(did.uri)#0",
type: "JsonWebKey2020",
controller: did.uri,
publicKeyJwk: jwk
publicKeyJwk: publicKey
)

let didDocument = DIDDocument(
return DIDDocument(
context: .list([
.string("https://www.w3.org/ns/did/v1"),
.string("https://w3id.org/security/suites/jws-2020/v1"),
Expand All @@ -72,7 +121,20 @@ public enum DIDJWK {
capabilityDelegation: [.referenced(verifiationMethod.id)],
capabilityInvocation: [.referenced(verifiationMethod.id)]
)
}
}

return DIDResolutionResult(didDocument: didDocument)
// MARK: - Errors

extension DIDJWK {
public enum Error: LocalizedError {
case importError(String)

public var errorDescription: String? {
switch self {
case let .importError(context):
return "Import error: \(context)"
}
}
}
}
26 changes: 17 additions & 9 deletions Sources/Web5/Dids/PortableDID.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import AnyCodable
import Foundation

/// A representation of a `BearerDID` that can be moved imported/exported.
///
/// `PortableDID` bundles all of the necessary information for a `BearerDID`,
/// enabling the usage of the DID in different context. This format is compatible
/// and interoperable across all Web5 programming languages.
public struct PortableDID: Codable {

public typealias Metadata = [String: AnyCodable]

/// URI of DID
let uri: String
let verificationMethods: [VerificationMethodKeyPair]

public struct VerificationMethodKeyPair: Codable {
let publicKey: Jwk
let privateKey: Jwk
/// `DIDDocument` of the DID
let document: DIDDocument

/// Private keys that correspond to the public keys present in the `document`
let privateKeys: [Jwk]

enum CodingKeys: String, CodingKey {
case publicKey = "publicKeyJwk"
case privateKey = "privateKeyJwk"
}
}
/// Additional DID method specific information to be included
let metadata: Metadata?
}
19 changes: 7 additions & 12 deletions Tests/Web5Tests/Dids/BearerDIDTests.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import CustomDump
import XCTest

@testable import Web5

final class BearerDIDTests: XCTestCase {

func test_toKeys() async throws {
func test_export() async throws {
let didJWK = try DIDJWK.create(keyManager: InMemoryKeyManager())
let portableDID = try await didJWK.toPortableDID()
let portableDID = try await didJWK.export()

XCTAssertEqual(portableDID.uri, didJWK.uri)
XCTAssertEqual(portableDID.verificationMethods.count, 1)
XCTAssertNoDifference(portableDID.uri, didJWK.uri)
XCTAssertNoDifference(portableDID.document, didJWK.document)
XCTAssertNoDifference(portableDID.privateKeys.count, 1)
XCTAssertNil(portableDID.metadata)
}

func test_initializeWithKeys() async throws {
let didJWK = try DIDJWK.create(keyManager: InMemoryKeyManager())
let portableDID = try await didJWK.toPortableDID()

XCTAssertNoThrow(try BearerDID(portableDID: portableDID))
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CustomDump
import XCTest

@testable import Web5
Expand Down Expand Up @@ -73,4 +74,19 @@ final class DIDJWKTests: XCTestCase {
XCTAssertNil(resolutionResult.didResolutionMetadata.error)
}

func test_import() throws {
let keyManager = InMemoryKeyManager()
let bearerDID = try DIDJWK.create(keyManager: keyManager)
let portableDID = try bearerDID.export()

let importedBearerDID = try DIDJWK.import(
portableDID: portableDID,
keyManager: keyManager
)

XCTAssertNoDifference(importedBearerDID.did, bearerDID.did)
XCTAssertNoDifference(importedBearerDID.document, bearerDID.document)
XCTAssertNoDifference(importedBearerDID.metadata, importedBearerDID.metadata)
}

}

0 comments on commit f24e37b

Please sign in to comment.