Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PortableDID refactoring part 1 #6

Merged
merged 1 commit into from
Feb 6, 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
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)
}

}
Loading