From 81083b63320fe2272a892529c31ca55f6a3795bd Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Wed, 3 Jan 2024 15:48:38 -0700 Subject: [PATCH 1/4] did:jwk creation and resolution --- Sources/tbDEX/Dids/Did.swift | 6 + Sources/tbDEX/Dids/DidDocument.swift | 286 +++++++++++++++++++++ Sources/tbDEX/Dids/DidJwk.swift | 59 +++++ Sources/tbDEX/Dids/DidResolution.swift | 78 ++++++ Sources/tbDEX/Dids/ParsedDid.swift | 44 ++++ Sources/tbDEX/crypto/Ed25519.swift | 6 +- Sources/tbDEX/crypto/Secp256k1.swift | 6 +- Sources/tbDEX/extensions/Base64URL.swift | 18 +- Tests/tbDEXTests/Dids/DidJwkTests.swift | 50 ++++ Tests/tbDEXTests/Dids/ParsedDidTests.swift | 27 ++ 10 files changed, 572 insertions(+), 8 deletions(-) create mode 100644 Sources/tbDEX/Dids/Did.swift create mode 100644 Sources/tbDEX/Dids/DidDocument.swift create mode 100644 Sources/tbDEX/Dids/DidJwk.swift create mode 100644 Sources/tbDEX/Dids/DidResolution.swift create mode 100644 Sources/tbDEX/Dids/ParsedDid.swift create mode 100644 Tests/tbDEXTests/Dids/DidJwkTests.swift create mode 100644 Tests/tbDEXTests/Dids/ParsedDidTests.swift diff --git a/Sources/tbDEX/Dids/Did.swift b/Sources/tbDEX/Dids/Did.swift new file mode 100644 index 0000000..526b89a --- /dev/null +++ b/Sources/tbDEX/Dids/Did.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol Did { + var uri: String { get } + var keyManager: KeyManager { get } +} diff --git a/Sources/tbDEX/Dids/DidDocument.swift b/Sources/tbDEX/Dids/DidDocument.swift new file mode 100644 index 0000000..4986b74 --- /dev/null +++ b/Sources/tbDEX/Dids/DidDocument.swift @@ -0,0 +1,286 @@ +import Foundation + +/// Decentralized Identifier (DID) Document +/// +/// A set of data describing the DID subject including mechanisms such as: +/// * cryptographic public keys - used to authenticate itself and prove association +/// with the DID +/// * services - means of communicating or interacting with the DID subject or associated +/// entities via one or more service endpoints. Examples include discovery services, agent +/// services, social networking services, file storage services, and verifiable credential +/// repository services. +/// +/// A DID Document can be retrieved by _resolving_ a DID URI +struct DidDocument: Codable { + + let context: String? + + /// The DID URI for a particular DID subject is expressed using the id property in the DID document. + let id: String + + /// A DID subject can have multiple identifiers for different purposes, or at + /// different times. The assertion that two or more DIDs (or other types of URI) + /// refer to the same DID subject can be made using the alsoKnownAs property. + var alsoKnownAs: [String]? + + /// A DID controller is an entity that is authorized to make changes to a + /// DID document. The process of authorizing a DID controller is defined + /// by the DID method. + var controller: DidController? + + /// Cryptographic public keys, which can be used to authenticate or authorize + /// interactions with the DID subject or associated parties. + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#verification-methods) + var verificationMethod: [DidVerificationMethod]? + + /// Services are used in DID documents to express ways of communicating with + /// the DID subject or associated entities. + /// A service can be any type of service the DID subject wants to advertise. + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#services) + var service: [DidService]? + + /// The assertionMethod verification relationship is used to specify how the + /// DID subject is expected to express claims, such as for the purposes of + /// issuing a Verifiable Credential + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#assertion) + var assertionMethod: [String]? + + /// The authentication verification relationship is used to specify how the + /// DID subject is expected to be authenticated, for purposes such as logging + /// into a website or engaging in any sort of challenge-response protocol. + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#authentication) + var authentication: [String]? + + /// The keyAgreement verification relationship is used to specify how an + /// entity can generate encryption material in order to transmit confidential + /// information intended for the DID subject, such as for the purposes of + /// establishing a secure communication channel with the recipient + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#key-agreement) + var keyAgreement: [String]? + + /// The capabilityDelegation verification relationship is used to specify a + /// mechanism that might be used by the DID subject to delegate a + /// cryptographic capability to another party, such as delegating the + /// authority to access a specific HTTP API to a subordinate. + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#capability-delegation) + var capabilityDelegation: [String]? + + /// The capabilityInvocation verification relationship is used to specify a + /// verification method that might be used by the DID subject to invoke a + /// cryptographic capability, such as the authorization to update the + /// DID Document + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#capability-invocation) + var capabilityInvocation: [String]? + + init( + context: String? = nil, + id: String, + alsoKnownAs: [String]? = nil, + controller: DidController? = nil, + verificationMethod: [DidVerificationMethod]? = nil, + service: [DidService]? = nil, + assertionMethod: [String]? = nil, + authentication: [String]? = nil, + keyAgreement: [String]? = nil, + capabilityDelegation: [String]? = nil, + capabilityInvocation: [String]? = nil + ) { + self.context = context + self.id = id + self.alsoKnownAs = alsoKnownAs + self.controller = controller + self.verificationMethod = verificationMethod + self.service = service + self.assertionMethod = assertionMethod + self.authentication = authentication + self.keyAgreement = keyAgreement + self.capabilityDelegation = capabilityDelegation + self.capabilityInvocation = capabilityInvocation + } + + enum CodingKeys: String, CodingKey { + case context = "@context" + case id + case alsoKnownAs + case controller + case verificationMethod + case service + case assertionMethod + case authentication + case keyAgreement + case capabilityDelegation + case capabilityInvocation + } + + /// Contains metadata about the DID document contained in the didDocument + /// property. This metadata typically does not change between invocations of + /// the resolve and resolveRepresentation functions unless the DID document + /// changes, as it represents metadata about the DID document. + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata) + struct Metadata: Codable { + + /// Timestamp of the Create operation. The value of the property MUST be a + /// string formatted as an XML Datetime normalized to UTC 00:00:00 and + /// without sub-second decimal precision. For example: 2020-12-20T19:17:47Z. + let created: String? + + /// Timestamp of the last Update operation for the document version which was + /// resolved. The value of the property MUST follow the same formatting rules + /// as the created property. The updated property is omitted if an Update + /// operation has never been performed on the DID document. If an updated + /// property exists, it can be the same value as the created property + /// when the difference between the two timestamps is less than one second. + let updated: String? + + /// If a DID has been deactivated, DID document metadata MUST include this + /// property with the boolean value true. If a DID has not been deactivated, + /// this property is OPTIONAL, but if included, MUST have the boolean value + /// false. + let deactivated: Bool? + + /// Indicates the version of the last Update operation for the document version + /// which was resolved. + let versionId: String? + + /// Indicates the timestamp of the next Update operation. The value of the + /// property MUST follow the same formatting rules as the created property. + let nextUpdate: String? + + /// If the resolved document version is not the latest version of the document. + /// It indicates the timestamp of the next Update operation. The value of the + /// property MUST follow the same formatting rules as the created property. + let nextVersionId: String? + + /// A DID method can define different forms of a DID that are logically + /// equivalent. An example is when a DID takes one form prior to registration + /// in a verifiable data registry and another form after such registration. + /// In this case, the DID method specification might need to express one or + /// more DIDs that are logically equivalent to the resolved DID as a property + /// of the DID document. This is the purpose of the equivalentId property. + let equivalentId: String? + + /// The canonicalId property is identical to the equivalentId property except: + /// * It is associated with a single value rather than a set + /// * The DID is defined to be the canonical ID for the DID subject within + /// the scope of the containing DID document. + let canonicalId: String? + + internal init( + created: String? = nil, + updated: String? = nil, + deactivated: Bool? = nil, + versionId: String? = nil, + nextUpdate: String? = nil, + nextVersionId: String? = nil, + equivalentId: String? = nil, + canonicalId: String? = nil + ) { + self.created = created + self.updated = updated + self.deactivated = deactivated + self.versionId = versionId + self.nextUpdate = nextUpdate + self.nextVersionId = nextVersionId + self.equivalentId = equivalentId + self.canonicalId = canonicalId + } + } +} + +/// DID Controller +/// +/// [Specification Reference](https://www.w3.org/TR/did-core/#did-controller) +/// This is necessary, as the controller can be either a String, or a set of strings. +/// Swift does not allow multiple types, and must handle both cases when encoding/decoding. +struct DidController: Codable { + var value: Either + + init(_ value: Either) { + self.value = value + } + + enum CodingKeys: CodingKey { + case value + } + + enum Either { + case left(A) + case right(B) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let singleValue = try? container.decode(String.self) { + self.value = .left(singleValue) + } else if let arrayValue = try? container.decode([String].self) { + self.value = .right(arrayValue) + } else { + throw DecodingError.typeMismatch( + DidController.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected either String or [String]" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch value { + case .left(let singleValue): + try container.encode(singleValue) + case .right(let arrayValue): + try container.encode(arrayValue) + } + } +} + +/// A DID document can express verification methods, such as cryptographic +/// public keys, which can be used to authenticate or authorize interactions +/// with the DID subject or associated parties. For example, +/// a cryptographic public key can be used as a verification method with +/// respect to a digital signature; in such usage, it verifies that the +/// signer could use the associated cryptographic private key +/// +/// [Specification Reference](https://www.w3.org/TR/did-core/#verification-methods) +struct DidVerificationMethod: Codable { + let id: String + let type: String + let controller: String + let publicKeyJwk: Jwk? + let publicKeyMultibase: String? + + init( + id: String, + type: String, + controller: String, + publicKeyJwk: Jwk? = nil, + publicKeyMultibase: String? = nil + ) { + self.id = id + self.type = type + self.controller = controller + self.publicKeyJwk = publicKeyJwk + self.publicKeyMultibase = publicKeyMultibase + } +} + +/// Services are used in DID documents to express ways of communicating with +/// the DID subject or associated entities. +/// A service can be any type of service the DID subject wants to advertise. +/// +/// [Specification Reference](https://www.w3.org/TR/did-core/#services) +struct DidService: Codable { + let id: String + let type: String + let serviceEndpoint: String +} diff --git a/Sources/tbDEX/Dids/DidJwk.swift b/Sources/tbDEX/Dids/DidJwk.swift new file mode 100644 index 0000000..90a10e4 --- /dev/null +++ b/Sources/tbDEX/Dids/DidJwk.swift @@ -0,0 +1,59 @@ +import Foundation + +struct DidJwk: Did { + + let uri: String + let keyManager: KeyManager + + // TODO: amika - add in opitons to allow caller to specify algorithm and curve + init(keyManager: KeyManager) throws { + let keyAlias = try keyManager.generatePrivateKey(algorithm: .eddsa, curve: .ed25519) + let publicKey = try keyManager.getPublicKey(keyAlias: keyAlias) + let publicKeyBase64Url = try JSONEncoder().encode(publicKey).base64UrlEncodedString() + + self.uri = "did:jwk:\(publicKeyBase64Url)" + self.keyManager = keyManager + } + + /// 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) -> DidResolution.Result { + let parsedDid: ParsedDid + do { + parsedDid = try ParsedDid(uri: didUri) + } catch { + return DidResolution.Result.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() + } + + let verifiationMethod = DidVerificationMethod( + id: "\(didUri)#0", + type: "JsonWebKey2020", + controller: didUri, + publicKeyJwk: jwk + ) + + let didDocument = DidDocument( + id: didUri, + verificationMethod: [verifiationMethod], + assertionMethod: [verifiationMethod.id], + authentication: [verifiationMethod.id], + capabilityDelegation: [verifiationMethod.id], + capabilityInvocation: [verifiationMethod.id] + ) + + return DidResolution.Result(didDocument: didDocument) + } +} diff --git a/Sources/tbDEX/Dids/DidResolution.swift b/Sources/tbDEX/Dids/DidResolution.swift new file mode 100644 index 0000000..62b5fd0 --- /dev/null +++ b/Sources/tbDEX/Dids/DidResolution.swift @@ -0,0 +1,78 @@ +import Foundation + +enum DidResolution { + + /// Representation of the result of a DID (Decentralized Identifier) resolution + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#resolution) + struct Result { + + /// The metadata associated with the DID resolution process. + /// + /// This includes information about the resolution process itself, such as any errors + /// that occurred. If not provided in the constructor, it defaults to an empty + /// `DidResolution.Metadata`. + let didResolutionMetadata: DidResolution.Metadata + + /// The resolved DID document, if available. + /// + /// This is the document that represents the resolved state of the DID. It may be `null` + /// if the DID could not be resolved or if the document is not available. + let didDocument: DidDocument? + + /// The metadata associated with the DID document. + /// + /// This includes information about the document such as when it was created and + /// any other relevant metadata. If not provided in the constructor, it defaults to an + /// empty `DidDocument.Metadata`. + let didDocumentMetadata: DidDocument.Metadata + + init( + didResolutionMetadata: DidResolution.Metadata = DidResolution.Metadata(), + didDocument: DidDocument? = nil, + didDocumentMetadata: DidDocument.Metadata = DidDocument.Metadata() + ) { + self.didResolutionMetadata = didResolutionMetadata + self.didDocument = didDocument + self.didDocumentMetadata = didDocumentMetadata + } + + static func invalidDid() -> Result { + Result( + didResolutionMetadata: Metadata(error: "invalidDid"), + didDocument: nil, + didDocumentMetadata: DidDocument.Metadata() + ) + } + } + + /// A metadata structure consisting of values relating to the results of the + /// DID resolution process which typically changes between invocations of the + /// resolve and resolveRepresentation functions, as it represents data about + /// the resolution process itself + /// + /// [Specification Reference](https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata) + struct Metadata: Codable { + + /// The Media Type of the returned didDocumentStream. This property is + /// REQUIRED if resolution is successful and if the resolveRepresentation + /// function was called. + let contentType: String? + + /// The error code from the resolution process. This property is REQUIRED + /// when there is an error in the resolution process. The value of this + /// property MUST be a single keyword ASCII string. The possible property + /// values of this field SHOULD be registered in the + /// [DID Specification Registries](https://www.w3.org/TR/did-spec-registries/#error) + let error: String? + + init( + contentType: String? = nil, + error: String? = nil + ) { + self.contentType = contentType + self.error = error + } + } + +} diff --git a/Sources/tbDEX/Dids/ParsedDid.swift b/Sources/tbDEX/Dids/ParsedDid.swift new file mode 100644 index 0000000..f9b4d8e --- /dev/null +++ b/Sources/tbDEX/Dids/ParsedDid.swift @@ -0,0 +1,44 @@ +import Foundation + +enum ParsedDidError: Error { + case invalidUri +} + +/// Parsed Decentralized Identifier (DID) URI, 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. + /// + /// Example: if the `uri` is `did:example:123456`, "example" would be the method name + private(set) var method: String + + /// The 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._%-]+)*)"# + + /// 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 { + throw ParsedDidError.invalidUri + } + + let methodRange = Range(match.range(at: 1), in: uri)! + let methodSpecificIdRange = Range(match.range(at: 2), in: uri)! + + self.uri = uri + self.method = String(uri[methodRange]) + self.id = String(uri[methodSpecificIdRange]) + } +} diff --git a/Sources/tbDEX/crypto/Ed25519.swift b/Sources/tbDEX/crypto/Ed25519.swift index e841b92..4e853b1 100644 --- a/Sources/tbDEX/crypto/Ed25519.swift +++ b/Sources/tbDEX/crypto/Ed25519.swift @@ -85,7 +85,8 @@ extension Ed25519: KeyGenerator { private func generatePrivateJwk(privateKey: Curve25519.Signing.PrivateKey) throws -> Jwk { var jwk = Jwk( - keyType: .octetKeyPair, + keyType: self.keyType, + algorithm: self.algorithm, curve: .ed25519, d: privateKey.rawRepresentation.base64UrlEncodedString(), x: privateKey.publicKey.rawRepresentation.base64UrlEncodedString() @@ -98,7 +99,8 @@ extension Ed25519: KeyGenerator { private func generatePublicJwk(publicKey: Curve25519.Signing.PublicKey) throws -> Jwk { var jwk = Jwk( - keyType: .octetKeyPair, + keyType: self.keyType, + algorithm: self.algorithm, curve: .ed25519, x: publicKey.rawRepresentation.base64UrlEncodedString() ) diff --git a/Sources/tbDEX/crypto/Secp256k1.swift b/Sources/tbDEX/crypto/Secp256k1.swift index 5d566f7..fd8c428 100644 --- a/Sources/tbDEX/crypto/Secp256k1.swift +++ b/Sources/tbDEX/crypto/Secp256k1.swift @@ -195,7 +195,8 @@ extension Secp256k1: KeyGenerator { let (x, y) = try getCurvePoints(keyBytes: privateKey.dataRepresentation) var jwk = Jwk( - keyType: .elliptic, + keyType: self.keyType, + algorithm: self.algorithm, curve: .secp256k1, d: privateKey.dataRepresentation.base64UrlEncodedString(), x: x.base64UrlEncodedString(), @@ -211,7 +212,8 @@ extension Secp256k1: KeyGenerator { let (x, y) = try getCurvePoints(keyBytes: publicKey.dataRepresentation) var jwk = Jwk( - keyType: .elliptic, + keyType: self.keyType, + algorithm: self.algorithm, curve: .secp256k1, x: x.base64UrlEncodedString(), y: y.base64UrlEncodedString() diff --git a/Sources/tbDEX/extensions/Base64URL.swift b/Sources/tbDEX/extensions/Base64URL.swift index 661286d..14a4b9e 100644 --- a/Sources/tbDEX/extensions/Base64URL.swift +++ b/Sources/tbDEX/extensions/Base64URL.swift @@ -3,14 +3,24 @@ import Foundation extension Collection where Element == UInt8 { /// Encodes a collection of bytes to a Base64URL encoded string - func base64UrlEncodedString() -> String { - Base64.encodeString(bytes: self, options: [.base64UrlAlphabet, .omitPaddingCharacter]) + func base64UrlEncodedString(padding: Bool = false) -> String { + let options: Base64.EncodingOptions = + padding + ? [.base64UrlAlphabet] + : [.base64UrlAlphabet, .omitPaddingCharacter] + + return Base64.encodeString(bytes: self, options: options) } } extension String { /// Decodes a Base64URL encoded string into bytes - func decodeBase64Url() throws -> Data { - Data(try Base64.decode(string: self, options: [.base64UrlAlphabet, .omitPaddingCharacter])) + func decodeBase64Url(padding: Bool = false) throws -> Data { + let options: Base64.DecodingOptions = + padding + ? [.base64UrlAlphabet] + : [.base64UrlAlphabet, .omitPaddingCharacter] + + return Data(try Base64.decode(string: self, options: options)) } } diff --git a/Tests/tbDEXTests/Dids/DidJwkTests.swift b/Tests/tbDEXTests/Dids/DidJwkTests.swift new file mode 100644 index 0000000..2922cc0 --- /dev/null +++ b/Tests/tbDEXTests/Dids/DidJwkTests.swift @@ -0,0 +1,50 @@ +import XCTest + +@testable import tbDEX + +final class DidJwkTests: XCTestCase { + + func test_initializer() throws { + let keyManager = InMemoryKeyManager() + let didJwk = try DidJwk(keyManager: keyManager) + + XCTAssert(didJwk.uri.starts(with: "did:jwk:")) + } + + func test_resolveWithError_onInvalidDidUri() throws { + let resolutionResult = DidJwk.resolve(didUri: "hi") + + XCTAssertNil(resolutionResult.didDocument) + XCTAssertEqual(resolutionResult.didResolutionMetadata.error, "invalidDid") + } + + func test_resolveWithError_ifDidUriNotJwk() { + let resolutionResult = DidJwk.resolve(didUri: "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH") + + XCTAssertNil(resolutionResult.didDocument) + XCTAssertEqual(resolutionResult.didResolutionMetadata.error, "invalidDid") + } + + func test_resolveWithError_ifDidUriIsNotValidBase64Url() { + let resolutionResult = DidJwk.resolve(didUri: "did:jwk:!!!") + + XCTAssertNil(resolutionResult.didDocument) + XCTAssertEqual(resolutionResult.didResolutionMetadata.error, "invalidDid") + } + + func test_resolveWithDidDocument() { + let didUri = + "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ" + let resolutionResult = DidJwk.resolve(didUri: didUri) + + XCTAssertNotNil(resolutionResult.didDocument) + XCTAssertEqual(resolutionResult.didDocument?.id, didUri) + XCTAssertEqual(resolutionResult.didDocument?.verificationMethod?.first?.id, "\(didUri)#0") + XCTAssertEqual(resolutionResult.didDocument?.authentication?.first, "\(didUri)#0") + XCTAssertEqual(resolutionResult.didDocument?.assertionMethod?.first, "\(didUri)#0") + XCTAssertEqual(resolutionResult.didDocument?.capabilityDelegation?.first, "\(didUri)#0") + XCTAssertEqual(resolutionResult.didDocument?.capabilityInvocation?.first, "\(didUri)#0") + XCTAssertNil(resolutionResult.didResolutionMetadata.error) + } + +} diff --git a/Tests/tbDEXTests/Dids/ParsedDidTests.swift b/Tests/tbDEXTests/Dids/ParsedDidTests.swift new file mode 100644 index 0000000..c518a9d --- /dev/null +++ b/Tests/tbDEXTests/Dids/ParsedDidTests.swift @@ -0,0 +1,27 @@ +import XCTest + +@testable import tbDEX + +class ParsedDidTests: XCTestCase { + + func test_initValidUri() throws { + let uri = "did:example:123abc" + let parsed = try ParsedDid(uri: uri) + XCTAssertEqual(parsed.uri, uri) + XCTAssertEqual(parsed.method, "example") + XCTAssertEqual(parsed.id, "123abc") + } + + func test_initValidUriWithParameters() throws { + let uri = "did:example:123abc;param=value/path?query#fragment" + let parsed = try ParsedDid(uri: uri) + XCTAssertEqual(parsed.uri, "did:example:123abc;param=value/path?query#fragment") + XCTAssertEqual(parsed.method, "example") + XCTAssertEqual(parsed.id, "123abc") + } + + func test_initInvalidUri() throws { + let uri = "invalid:uri" + XCTAssertThrowsError(try ParsedDid(uri: uri)) + } +} From dc5081f680cc75d9d66bbe19fa64abee5b4479bb Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Wed, 3 Jan 2024 15:55:09 -0700 Subject: [PATCH 2/4] Allow caller to specify algorithm/curve combo --- Sources/tbDEX/Dids/DidJwk.swift | 10 +++++++--- Tests/tbDEXTests/Dids/DidJwkTests.swift | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sources/tbDEX/Dids/DidJwk.swift b/Sources/tbDEX/Dids/DidJwk.swift index 90a10e4..607014e 100644 --- a/Sources/tbDEX/Dids/DidJwk.swift +++ b/Sources/tbDEX/Dids/DidJwk.swift @@ -2,12 +2,16 @@ import Foundation struct DidJwk: Did { + struct Options { + let algorithm: Jwk.Algorithm + let curve: Jwk.Curve + } + let uri: String let keyManager: KeyManager - // TODO: amika - add in opitons to allow caller to specify algorithm and curve - init(keyManager: KeyManager) throws { - let keyAlias = try keyManager.generatePrivateKey(algorithm: .eddsa, curve: .ed25519) + init(keyManager: KeyManager, options: Options) throws { + let keyAlias = try keyManager.generatePrivateKey(algorithm: options.algorithm, curve: options.curve) let publicKey = try keyManager.getPublicKey(keyAlias: keyAlias) let publicKeyBase64Url = try JSONEncoder().encode(publicKey).base64UrlEncodedString() diff --git a/Tests/tbDEXTests/Dids/DidJwkTests.swift b/Tests/tbDEXTests/Dids/DidJwkTests.swift index 2922cc0..aae41fe 100644 --- a/Tests/tbDEXTests/Dids/DidJwkTests.swift +++ b/Tests/tbDEXTests/Dids/DidJwkTests.swift @@ -6,7 +6,10 @@ final class DidJwkTests: XCTestCase { func test_initializer() throws { let keyManager = InMemoryKeyManager() - let didJwk = try DidJwk(keyManager: keyManager) + let didJwk = try DidJwk( + keyManager: keyManager, + options: .init(algorithm: .eddsa, curve: .ed25519) + ) XCTAssert(didJwk.uri.starts(with: "did:jwk:")) } From 4eaca1688b9554b200cc6c00482138953178f551 Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Wed, 3 Jan 2024 16:17:51 -0700 Subject: [PATCH 3/4] Don't set alg on newly created Jwks, as it breaks test vectors --- Sources/tbDEX/crypto/Crypto.swift | 3 ++- Sources/tbDEX/crypto/Ed25519.swift | 6 ++---- Sources/tbDEX/crypto/Secp256k1.swift | 6 ++---- Sources/tbDEX/extensions/Base64URL.swift | 8 ++++---- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Sources/tbDEX/crypto/Crypto.swift b/Sources/tbDEX/crypto/Crypto.swift index e9d9987..76cfff6 100644 --- a/Sources/tbDEX/crypto/Crypto.swift +++ b/Sources/tbDEX/crypto/Crypto.swift @@ -74,7 +74,8 @@ enum Crypto { (Secp256k1.shared.algorithm, nil), (Secp256k1.shared.algorithm, .secp256k1): return Secp256k1.shared - case (Ed25519.shared.algorithm, .ed25519): + case (Ed25519.shared.algorithm, .ed25519), + (nil, .ed25519): return Ed25519.shared default: throw CryptoError.illegalArgument( diff --git a/Sources/tbDEX/crypto/Ed25519.swift b/Sources/tbDEX/crypto/Ed25519.swift index 4e853b1..e841b92 100644 --- a/Sources/tbDEX/crypto/Ed25519.swift +++ b/Sources/tbDEX/crypto/Ed25519.swift @@ -85,8 +85,7 @@ extension Ed25519: KeyGenerator { private func generatePrivateJwk(privateKey: Curve25519.Signing.PrivateKey) throws -> Jwk { var jwk = Jwk( - keyType: self.keyType, - algorithm: self.algorithm, + keyType: .octetKeyPair, curve: .ed25519, d: privateKey.rawRepresentation.base64UrlEncodedString(), x: privateKey.publicKey.rawRepresentation.base64UrlEncodedString() @@ -99,8 +98,7 @@ extension Ed25519: KeyGenerator { private func generatePublicJwk(publicKey: Curve25519.Signing.PublicKey) throws -> Jwk { var jwk = Jwk( - keyType: self.keyType, - algorithm: self.algorithm, + keyType: .octetKeyPair, curve: .ed25519, x: publicKey.rawRepresentation.base64UrlEncodedString() ) diff --git a/Sources/tbDEX/crypto/Secp256k1.swift b/Sources/tbDEX/crypto/Secp256k1.swift index fd8c428..5d566f7 100644 --- a/Sources/tbDEX/crypto/Secp256k1.swift +++ b/Sources/tbDEX/crypto/Secp256k1.swift @@ -195,8 +195,7 @@ extension Secp256k1: KeyGenerator { let (x, y) = try getCurvePoints(keyBytes: privateKey.dataRepresentation) var jwk = Jwk( - keyType: self.keyType, - algorithm: self.algorithm, + keyType: .elliptic, curve: .secp256k1, d: privateKey.dataRepresentation.base64UrlEncodedString(), x: x.base64UrlEncodedString(), @@ -212,8 +211,7 @@ extension Secp256k1: KeyGenerator { let (x, y) = try getCurvePoints(keyBytes: publicKey.dataRepresentation) var jwk = Jwk( - keyType: self.keyType, - algorithm: self.algorithm, + keyType: .elliptic, curve: .secp256k1, x: x.base64UrlEncodedString(), y: y.base64UrlEncodedString() diff --git a/Sources/tbDEX/extensions/Base64URL.swift b/Sources/tbDEX/extensions/Base64URL.swift index 14a4b9e..a27568f 100644 --- a/Sources/tbDEX/extensions/Base64URL.swift +++ b/Sources/tbDEX/extensions/Base64URL.swift @@ -3,9 +3,9 @@ import Foundation extension Collection where Element == UInt8 { /// Encodes a collection of bytes to a Base64URL encoded string - func base64UrlEncodedString(padding: Bool = false) -> String { + func base64UrlEncodedString(padded: Bool = false) -> String { let options: Base64.EncodingOptions = - padding + padded ? [.base64UrlAlphabet] : [.base64UrlAlphabet, .omitPaddingCharacter] @@ -15,9 +15,9 @@ extension Collection where Element == UInt8 { extension String { /// Decodes a Base64URL encoded string into bytes - func decodeBase64Url(padding: Bool = false) throws -> Data { + func decodeBase64Url(padded: Bool = false) throws -> Data { let options: Base64.DecodingOptions = - padding + padded ? [.base64UrlAlphabet] : [.base64UrlAlphabet, .omitPaddingCharacter] From c41eab975f82616daaaaa0d42e4abfd396ed5b49 Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Wed, 3 Jan 2024 16:22:03 -0700 Subject: [PATCH 4/4] Additional test for resolving newly created did:jwk --- Tests/tbDEXTests/Dids/DidJwkTests.swift | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/tbDEXTests/Dids/DidJwkTests.swift b/Tests/tbDEXTests/Dids/DidJwkTests.swift index aae41fe..d324b1f 100644 --- a/Tests/tbDEXTests/Dids/DidJwkTests.swift +++ b/Tests/tbDEXTests/Dids/DidJwkTests.swift @@ -35,7 +35,25 @@ final class DidJwkTests: XCTestCase { XCTAssertEqual(resolutionResult.didResolutionMetadata.error, "invalidDid") } - func test_resolveWithDidDocument() { + func test_resolveNewlyCratedDidJwk() throws { + let keyManager = InMemoryKeyManager() + let didJwk = try DidJwk( + keyManager: keyManager, + options: .init(algorithm: .es256k, curve: .secp256k1) + ) + + let resolutionResult = DidJwk.resolve(didUri: didJwk.uri) + XCTAssertNotNil(resolutionResult.didDocument) + XCTAssertEqual(resolutionResult.didDocument?.id, didJwk.uri) + XCTAssertEqual(resolutionResult.didDocument?.verificationMethod?.first?.id, "\(didJwk.uri)#0") + XCTAssertEqual(resolutionResult.didDocument?.authentication?.first, "\(didJwk.uri)#0") + XCTAssertEqual(resolutionResult.didDocument?.assertionMethod?.first, "\(didJwk.uri)#0") + XCTAssertEqual(resolutionResult.didDocument?.capabilityDelegation?.first, "\(didJwk.uri)#0") + XCTAssertEqual(resolutionResult.didDocument?.capabilityInvocation?.first, "\(didJwk.uri)#0") + XCTAssertNil(resolutionResult.didResolutionMetadata.error) + } + + func test_resolveWithKnownDidUri() { let didUri = "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ" let resolutionResult = DidJwk.resolve(didUri: didUri)