From f24e37b2dd2d6d2b1316088caa040a0b86b805b8 Mon Sep 17 00:00:00 2001 From: Adam Mika <88001738+amika-sq@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:55:08 -0700 Subject: [PATCH] PortableDID refactoring part 1 (#6) --- Package.swift | 9 +- Sources/Web5/Dids/BearerDID.swift | 85 ++++++++++--------- Sources/Web5/Dids/DID.swift | 2 +- Sources/Web5/Dids/Methods/DIDJWK.swift | 70 ++++++++++++++- Sources/Web5/Dids/PortableDID.swift | 26 ++++-- Tests/Web5Tests/Dids/BearerDIDTests.swift | 19 ++--- .../{DidJwkTests.swift => DIDJWKTests.swift} | 16 ++++ 7 files changed, 157 insertions(+), 70 deletions(-) rename Tests/Web5Tests/Dids/{DidJwkTests.swift => DIDJWKTests.swift} (87%) diff --git a/Package.swift b/Package.swift index 0a29c2f..d5df100 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,8 @@ 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( @@ -26,11 +27,15 @@ let package = Package( 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", diff --git a/Sources/Web5/Dids/BearerDID.swift b/Sources/Web5/Dids/BearerDID.swift index d6ca96e..b954cd1 100644 --- a/Sources/Web5/Dids/BearerDID.swift +++ b/Sources/Web5/Dids/BearerDID.swift @@ -1,3 +1,4 @@ +import AnyCodable import Foundation // BearerDID is a composite type that combines a DID with a KeyManager containing keys @@ -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 @@ -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 ) } } @@ -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)" } } } diff --git a/Sources/Web5/Dids/DID.swift b/Sources/Web5/Dids/DID.swift index 0f76da0..ab6b0c4 100644 --- a/Sources/Web5/Dids/DID.swift +++ b/Sources/Web5/Dids/DID.swift @@ -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 /// diff --git a/Sources/Web5/Dids/Methods/DIDJWK.swift b/Sources/Web5/Dids/Methods/DIDJWK.swift index 94fae14..96e99cb 100644 --- a/Sources/Web5/Dids/Methods/DIDJWK.swift +++ b/Sources/Web5/Dids/Methods/DIDJWK.swift @@ -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` @@ -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"), @@ -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)" + } + } } } diff --git a/Sources/Web5/Dids/PortableDID.swift b/Sources/Web5/Dids/PortableDID.swift index 034692a..fafba10 100644 --- a/Sources/Web5/Dids/PortableDID.swift +++ b/Sources/Web5/Dids/PortableDID.swift @@ -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? } diff --git a/Tests/Web5Tests/Dids/BearerDIDTests.swift b/Tests/Web5Tests/Dids/BearerDIDTests.swift index 63612a3..64b70dd 100644 --- a/Tests/Web5Tests/Dids/BearerDIDTests.swift +++ b/Tests/Web5Tests/Dids/BearerDIDTests.swift @@ -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)) - } - } diff --git a/Tests/Web5Tests/Dids/DidJwkTests.swift b/Tests/Web5Tests/Dids/DIDJWKTests.swift similarity index 87% rename from Tests/Web5Tests/Dids/DidJwkTests.swift rename to Tests/Web5Tests/Dids/DIDJWKTests.swift index fd9004f..01ae9a4 100644 --- a/Tests/Web5Tests/Dids/DidJwkTests.swift +++ b/Tests/Web5Tests/Dids/DIDJWKTests.swift @@ -1,3 +1,4 @@ +import CustomDump import XCTest @testable import Web5 @@ -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) + } + }