From 7499b3c00661184de25469eaead301052991696a Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Thu, 4 Jan 2024 10:15:34 -0700 Subject: [PATCH] Implement web5 did:jwk test vectors --- Package.swift | 1 + Sources/tbDEX/Dids/DidDocument.swift | 65 ++--------- Sources/tbDEX/Dids/DidJwk.swift | 4 + Sources/tbDEX/Dids/DidResolution.swift | 4 +- .../{extensions => Utilities}/Base64URL.swift | 0 Sources/tbDEX/Utilities/OneOrMany.swift | 33 ++++++ .../Dids/Web5TestVectorsDidJwk.swift | 21 ++++ .../TestVectors/did_jwk/resolve.json | 107 ++++++++++++++++++ 8 files changed, 176 insertions(+), 59 deletions(-) rename Sources/tbDEX/{extensions => Utilities}/Base64URL.swift (100%) create mode 100644 Sources/tbDEX/Utilities/OneOrMany.swift create mode 100644 Tests/tbDEXTests/Dids/Web5TestVectorsDidJwk.swift create mode 100644 Tests/tbDEXTests/TestVectors/did_jwk/resolve.json diff --git a/Package.swift b/Package.swift index 3f2fff0..76d5d06 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( resources: [ .copy("TestVectors/ed25519"), .copy("TestVectors/secp256k1"), + .copy("TestVectors/did_jwk"), ] ), ] diff --git a/Sources/tbDEX/Dids/DidDocument.swift b/Sources/tbDEX/Dids/DidDocument.swift index 4986b74..d2207dd 100644 --- a/Sources/tbDEX/Dids/DidDocument.swift +++ b/Sources/tbDEX/Dids/DidDocument.swift @@ -11,9 +11,9 @@ import Foundation /// repository services. /// /// A DID Document can be retrieved by _resolving_ a DID URI -struct DidDocument: Codable { +struct DidDocument: Codable, Equatable { - let context: String? + let context: OneOrMany? /// The DID URI for a particular DID subject is expressed using the id property in the DID document. let id: String @@ -26,7 +26,7 @@ struct DidDocument: Codable { /// 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? + var controller: OneOrMany? /// Cryptographic public keys, which can be used to authenticate or authorize /// interactions with the DID subject or associated parties. @@ -80,10 +80,10 @@ struct DidDocument: Codable { var capabilityInvocation: [String]? init( - context: String? = nil, + context: OneOrMany? = nil, id: String, alsoKnownAs: [String]? = nil, - controller: DidController? = nil, + controller: OneOrMany? = nil, verificationMethod: [DidVerificationMethod]? = nil, service: [DidService]? = nil, assertionMethod: [String]? = nil, @@ -125,7 +125,7 @@ struct DidDocument: Codable { /// changes, as it represents metadata about the DID document. /// /// [Specification Reference](https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata) - struct Metadata: Codable { + struct Metadata: Codable, Equatable { /// 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 @@ -195,55 +195,6 @@ struct DidDocument: Codable { } } -/// 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, @@ -252,7 +203,7 @@ struct DidController: Codable { /// signer could use the associated cryptographic private key /// /// [Specification Reference](https://www.w3.org/TR/did-core/#verification-methods) -struct DidVerificationMethod: Codable { +struct DidVerificationMethod: Codable, Equatable { let id: String let type: String let controller: String @@ -279,7 +230,7 @@ struct DidVerificationMethod: Codable { /// 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 { +struct DidService: Codable, Equatable { let id: String let type: String let serviceEndpoint: String diff --git a/Sources/tbDEX/Dids/DidJwk.swift b/Sources/tbDEX/Dids/DidJwk.swift index 607014e..bd944bd 100644 --- a/Sources/tbDEX/Dids/DidJwk.swift +++ b/Sources/tbDEX/Dids/DidJwk.swift @@ -50,6 +50,10 @@ struct DidJwk: Did { ) let didDocument = DidDocument( + context: .many([ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + ]), id: didUri, verificationMethod: [verifiationMethod], assertionMethod: [verifiationMethod.id], diff --git a/Sources/tbDEX/Dids/DidResolution.swift b/Sources/tbDEX/Dids/DidResolution.swift index 62b5fd0..0ec6996 100644 --- a/Sources/tbDEX/Dids/DidResolution.swift +++ b/Sources/tbDEX/Dids/DidResolution.swift @@ -5,7 +5,7 @@ enum DidResolution { /// Representation of the result of a DID (Decentralized Identifier) resolution /// /// [Specification Reference](https://www.w3.org/TR/did-core/#resolution) - struct Result { + struct Result: Codable, Equatable { /// The metadata associated with the DID resolution process. /// @@ -52,7 +52,7 @@ enum DidResolution { /// the resolution process itself /// /// [Specification Reference](https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata) - struct Metadata: Codable { + struct Metadata: Codable, Equatable { /// The Media Type of the returned didDocumentStream. This property is /// REQUIRED if resolution is successful and if the resolveRepresentation diff --git a/Sources/tbDEX/extensions/Base64URL.swift b/Sources/tbDEX/Utilities/Base64URL.swift similarity index 100% rename from Sources/tbDEX/extensions/Base64URL.swift rename to Sources/tbDEX/Utilities/Base64URL.swift diff --git a/Sources/tbDEX/Utilities/OneOrMany.swift b/Sources/tbDEX/Utilities/OneOrMany.swift new file mode 100644 index 0000000..80223e2 --- /dev/null +++ b/Sources/tbDEX/Utilities/OneOrMany.swift @@ -0,0 +1,33 @@ +import Foundation + +enum OneOrMany: Codable, Equatable { + case one(T) + case many([T]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let singleValue = try? container.decode(T.self) { + self = .one(singleValue) + } else if let arrayValue = try? container.decode([T].self) { + self = .many(arrayValue) + } else { + throw DecodingError.typeMismatch( + OneOrMany.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected either \(T.self) or [\(T.self)]" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .one(let singleValue): + try container.encode(singleValue) + case .many(let arrayValue): + try container.encode(arrayValue) + } + } +} diff --git a/Tests/tbDEXTests/Dids/Web5TestVectorsDidJwk.swift b/Tests/tbDEXTests/Dids/Web5TestVectorsDidJwk.swift new file mode 100644 index 0000000..7d540bf --- /dev/null +++ b/Tests/tbDEXTests/Dids/Web5TestVectorsDidJwk.swift @@ -0,0 +1,21 @@ +import CustomDump +import XCTest + +@testable import tbDEX + +final class Web5TestVectorsDidJwk: XCTestCase { + + func test_resolve() throws { + let testVector: TestVector = try loadTestVector( + fileName: "resolve", + subdirectory: "did_jwk" + ) + + for vector in testVector.vectors { + let didUri = vector.input + let result = DidJwk.resolve(didUri: didUri) + XCTAssertNoDifference(result, vector.output) + } + } + +} diff --git a/Tests/tbDEXTests/TestVectors/did_jwk/resolve.json b/Tests/tbDEXTests/TestVectors/did_jwk/resolve.json new file mode 100644 index 0000000..a2142eb --- /dev/null +++ b/Tests/tbDEXTests/TestVectors/did_jwk/resolve.json @@ -0,0 +1,107 @@ +{ + "description": "did:jwk resolution test vectors", + "vectors": [ + { + "description": "resolves did:jwk 1", + "input": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "output": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0", + "controller": "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0", + "publicKeyJwk": { + "kty": "EC", + "use": "sig", + "crv": "secp256k1", + "kid": "i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", + "x": "vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U", + "y": "VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU", + "alg": "ES256K" + } + } + ], + "authentication": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "assertionMethod": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityInvocation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ], + "capabilityDelegation": [ + "did:jwk:eyJrdHkiOiJFQyIsInVzZSI6InNpZyIsImNydiI6InNlY3AyNTZrMSIsImtpZCI6ImkzU1BSQnRKS292SEZzQmFxTTkydGk2eFFDSkxYM0U3WUNld2lIVjJDU2ciLCJ4IjoidmRyYnoyRU96dmJMRFZfLWtMNGVKdDdWSS04VEZaTm1BOVlnV3p2aGg3VSIsInkiOiJWTEZxUU1aUF9Bc3B1Y1hvV1gyLWJHWHBBTzFmUTVMbjE5VjVSQXhyZ3ZVIiwiYWxnIjoiRVMyNTZLIn0#0" + ] + }, + "didDocumentMetadata": {}, + "didResolutionMetadata": {} + }, + "errors": false + }, + { + "description": "resolves did:jwk 2", + "input": "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ", + "output": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ", + "verificationMethod": [ + { + "type": "JsonWebKey2020", + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ#0", + "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ", + "publicKeyJwk": { + "kty": "OKP", + "use": "sig", + "crv": "Ed25519", + "kid": "VtSHXPlKDw1EEoOj5XN3XWhjSPYVNvX-e4vjRO0yYJA", + "x": "iz70svSLxNZhsDxeJQ_jnOVbX3KFNkcBcMjWjZmXEsA", + "alg": "EdDSA" + } + } + ], + "authentication": [ + "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ#0" + ], + "assertionMethod": [ + "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ#0" + ], + "capabilityInvocation": [ + "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ#0" + ], + "capabilityDelegation": [ + "did:jwk:eyJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiVnRTSFhQbEtEdzFFRW9PajVYTjNYV2hqU1BZVk52WC1lNHZqUk8weVlKQSIsIngiOiJpejcwc3ZTTHhOWmhzRHhlSlFfam5PVmJYM0tGTmtjQmNNaldqWm1YRXNBIiwiYWxnIjoiRWREU0EifQ#0" + ] + }, + "didDocumentMetadata": {}, + "didResolutionMetadata": {} + }, + "errors": false + }, + { + "description": "resolution for invalid did", + "input": "did:jwk:hehe", + "output": { + "@context": "https://w3id.org/did-resolution/v1", + "didDocument": null, + "didResolutionMetadata": { + "error": "invalidDid" + }, + "didDocumentMetadata": {} + }, + "errors": false + } + ] +}