diff --git a/build.gradle.kts b/build.gradle.kts index 1be9283b7..073c38652 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,8 +50,6 @@ allprojects { force("com.google.guava:guava:32.0.0-android") // Addresses https://github.com/TBD54566975/web5-kt/issues/244 force("com.squareup.okio:okio:3.6.0") - // Addresses https://github.com/TBD54566975/web5-kt/issues/257 - force("com.nimbusds:nimbus-jose-jwt:9.37.2") } } } @@ -101,20 +99,6 @@ subprojects { jvmTarget = "1.8" } - sourceSets { - create("intTest") { - compileClasspath += sourceSets.main.get().output - runtimeClasspath += sourceSets.main.get().output - } - } - - val intTestImplementation by configurations.getting { - extendsFrom(configurations.implementation.get()) - } - val intTestRuntimeOnly by configurations.getting - - configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get()) - dependencies { detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4") detektPlugins("com.github.TBD54566975:tbd-detekt-rules:v0.0.2") @@ -122,39 +106,8 @@ subprojects { testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - intTestImplementation(kotlin("test")) - intTestImplementation("org.junit.jupiter:junit-jupiter:5.9.2") - intTestRuntimeOnly("org.junit.platform:junit-platform-launcher") } - idea { - module { - testSources.from(sourceSets["intTest"].java.srcDirs) - testSources.from(sourceSets["intTest"].kotlin.srcDirs) - } - } - - val integrationTest = task("integrationTest") { - description = "Runs integration tests." - group = "verification" - - testClassesDirs = sourceSets["intTest"].output.classesDirs - classpath = sourceSets["intTest"].runtimeClasspath - shouldRunAfter("test") - - useJUnitPlatform() - - testLogging { - events("passed", "skipped", "failed", "standardOut", "standardError") - exceptionFormat = TestExceptionFormat.FULL - showExceptions = true - showCauses = true - showStackTraces = true - } - } - - tasks.check { dependsOn(integrationTest) } - detekt { config.setFrom("$rootDir/config/detekt.yml") } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index a2585e68f..6a5c58711 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { // Project // Implementation + implementation(libs.comFasterXmlJacksonModuleKotlin) // Test /** diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt new file mode 100644 index 000000000..60b4e0e3d --- /dev/null +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -0,0 +1,46 @@ +package web5.sdk.common + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.module.kotlin.registerKotlinModule + +/** + * A singleton for json serialization/deserialization, shared across the SDK as ObjectMapper instantiation + * is an expensive operation. + * - Serialize ([stringify]) + * + * ### Example Usage: + * ```kotlin + * val offering = Json.objectMapper.readValue(payload) + * + * val jsonString = Json.stringify(myObject) + * + * val node = Json.parse(payload) + * ``` + */ +public object Json { + /** + * The Jackson object mapper instance, shared across the lib. + * + * It must be public in order for typed parsing to work as we cannot use reified types for Java interop. + */ + public val jsonMapper: ObjectMapper = ObjectMapper() + .registerKotlinModule() + .findAndRegisterModules() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + private val objectWriter: ObjectWriter = jsonMapper.writer() + + /** + * Converts a kotlin object to a json string. + * + * @param obj The object to stringify. + * @return json string. + */ + public fun stringify(obj: Any): String { + return objectWriter.writeValueAsString(obj) + } +} \ No newline at end of file diff --git a/config/detekt.yml b/config/detekt.yml index 74b72f89d..855c8123a 100644 --- a/config/detekt.yml +++ b/config/detekt.yml @@ -48,6 +48,7 @@ complexity: CyclomaticComplexMethod: active: true ignoreSingleWhenExpression: true + threshold: 25 LongParameterList: constructorThreshold: 10 MethodOverloading: @@ -82,7 +83,7 @@ formatting: MaximumLineLength: active: true ImportOrdering: - active: true + active: false naming: MemberNameEqualsClassName: diff --git a/credentials/build.gradle.kts b/credentials/build.gradle.kts index 16f1683b2..b7671ea46 100644 --- a/credentials/build.gradle.kts +++ b/credentials/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { implementation(libs.comNetworkntJsonSchemaValidator) implementation(libs.comNfeldJsonpathkt) implementation(libs.comNimbusdsJoseJwt) - implementation(libs.decentralizedIdentityDidCommonJava) implementation(libs.bundles.ioKtorForCredentials) // Test diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 489c0d05a..f987bd374 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -55,7 +55,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * * @param did The [Did] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the - * produces signature. + * produced signature. * @return The JWT representing the signed verifiable credential. * * Example: diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index 64e50c00c..ffd4bc14d 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -31,10 +31,11 @@ public typealias VpDataModel = com.danubetech.verifiablecredentials.VerifiablePr * * @property vpDataModel The [vpDataModel] instance representing the core data model of a verifiable presentation. */ +@Suppress("UNCHECKED_CAST") public class VerifiablePresentation internal constructor(public val vpDataModel: VpDataModel) { public val verifiableCredential: List - get() = vpDataModel.toMap().get("verifiableCredential") as List + get() = vpDataModel.toMap()["verifiableCredential"] as List public val holder: String get() = vpDataModel.holder.toString() diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt index fd9e9ae1e..701b0453c 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt @@ -3,23 +3,22 @@ package web5.sdk.credentials.util import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.util.Base64URL import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT -import foundation.identity.did.DIDURL import web5.sdk.common.Convert import web5.sdk.crypto.Crypto -import web5.sdk.crypto.Jwa import web5.sdk.dids.Did import web5.sdk.dids.DidResolvers +import web5.sdk.dids.didcore.DidUri import web5.sdk.dids.exceptions.DidResolutionException -import web5.sdk.dids.findAssertionMethodById +import web5.sdk.dids.exceptions.PublicKeyJwkMissingException +import java.net.URI import java.security.SignatureException -private const val JsonWebKey2020 = "JsonWebKey2020" -private const val JsonWebKey = "JsonWebKey" +private const val JSON_WEB_KEY_2020 = "JsonWebKey2020" +private const val JSON_WEB_KEY = "JsonWebKey" /** * Util class for common shared JWT methods. @@ -56,16 +55,15 @@ public object JwtUtil { val assertionMethod = didDocument.findAssertionMethodById(assertionMethodId) - // TODO: ensure that publicKeyJwk is not null - val publicKeyJwk = JWK.parse(assertionMethod.publicKeyJwk) + val publicKeyJwk = assertionMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null.") val keyAlias = did.keyManager.getDeterministicAlias(publicKeyJwk) // TODO: figure out how to make more reliable since algorithm is technically not a required property of a JWK val algorithm = publicKeyJwk.algorithm val jwsAlgorithm = JWSAlgorithm.parse(algorithm.toString()) - val kid = when (assertionMethod.id.isAbsolute) { - true -> assertionMethod.id.toString() + val kid = when (URI.create(assertionMethod.id).isAbsolute) { + true -> assertionMethod.id false -> "${did.uri}${assertionMethod.id}" } @@ -110,13 +108,13 @@ public object JwtUtil { } val verificationMethodId = jwt.header.keyID - val parsedDidUrl = DIDURL.fromString(verificationMethodId) // validates vm id which is a DID URL + val didUri = DidUri.Parser.parse(verificationMethodId) - val didResolutionResult = DidResolvers.resolve(parsedDidUrl.did.didString) + val didResolutionResult = DidResolvers.resolve(didUri.url) if (didResolutionResult.didResolutionMetadata.error != null) { throw SignatureException( "Signature verification failed: " + - "Failed to resolve DID ${parsedDidUrl.did.didString}. " + + "Failed to resolve DID ${didUri.url}. " + "Error: ${didResolutionResult.didResolutionMetadata.error}" ) } @@ -124,28 +122,43 @@ public object JwtUtil { // create a set of possible id matches. the DID spec allows for an id to be the entire `did#fragment` // or just `#fragment`. See: https://www.w3.org/TR/did-core/#relative-did-urls. // using a set for fast string comparison. DIDs can be lonnng. - val verificationMethodIds = setOf(parsedDidUrl.didUrlString, "#${parsedDidUrl.fragment}") - val assertionMethods = didResolutionResult.didDocument?.assertionMethodVerificationMethodsDereferenced - val assertionMethod = assertionMethods?.firstOrNull { - val id = it.id.toString() - verificationMethodIds.contains(id) - } - ?: throw SignatureException( + val verificationMethodIds = setOf( + didUri.url, + "#${didUri.fragment}" + ) + + didResolutionResult.didDocument?.assertionMethod?.firstOrNull { + verificationMethodIds.contains(it) + } ?: throw SignatureException( + "Signature verification failed: Expected kid in JWS header to dereference " + + "a DID Document Verification Method with an Assertion verification relationship" + ) + + // TODO: this will be cleaned up as part of BearerDid PR + val assertionVerificationMethod = didResolutionResult + .didDocument + ?.verificationMethod + ?.find { verificationMethodIds.contains(it.id) } + + if (assertionVerificationMethod == null) { + throw SignatureException( "Signature verification failed: Expected kid in JWS header to dereference " + "a DID Document Verification Method with an Assertion verification relationship" ) + } - require((assertionMethod.isType(JsonWebKey2020) || assertionMethod.isType(JsonWebKey)) && - assertionMethod.publicKeyJwk != null) { + require( + (assertionVerificationMethod.isType(JSON_WEB_KEY_2020) || assertionVerificationMethod.isType(JSON_WEB_KEY)) && + assertionVerificationMethod.publicKeyJwk != null + ) { throw SignatureException( "Signature verification failed: Expected kid in JWS header to dereference " + - "a DID Document Verification Method of type $JsonWebKey2020 or $JsonWebKey with a publicKeyJwk" + "a DID Document Verification Method of type $JSON_WEB_KEY_2020 or $JSON_WEB_KEY with a publicKeyJwk" ) } - val publicKeyMap = assertionMethod.publicKeyJwk - val publicKeyJwk = JWK.parse(publicKeyMap) - + val publicKeyJwk = + assertionVerificationMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") val toVerifyBytes = jwt.signingInput val signatureBytes = jwt.signature.decode() @@ -155,4 +168,6 @@ public object JwtUtil { signatureBytes ) } + + } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index d3546613c..6c67601e8 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -33,9 +33,16 @@ class PresentationExchangeTest { .registerKotlinModule() .setSerializationInclusion(JsonInclude.Include.NON_NULL) - @Suppress("MaximumLineLength") val sanctionsVcJwt = - "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtrdU5tSmF0ZUNUZXI1V0JycUhCVUM0YUM3TjlOV1NyTURKNmVkQXY1V0NmMiIsInN1YiI6ImRpZDprZXk6ejZNa2t1Tm1KYXRlQ1RlcjVXQnJxSEJVQzRhQzdOOU5XU3JNREo2ZWRBdjVXQ2YyIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaWQiOiIxNjk4NDIyNDAxMzUyIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlNhbmN0aW9uc0NyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1ra3VObUphdGVDVGVyNVdCcnFIQlVDNGFDN045TldTck1ESjZlZEF2NVdDZjIiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTEwLTI3VDE2OjAwOjAxWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1ra3VObUphdGVDVGVyNVdCcnFIQlVDNGFDN045TldTck1ESjZlZEF2NVdDZjIiLCJiZWVwIjoiYm9vcCJ9fX0.Xhd9nDdkGarYFr6FP7wqsgj5CK3oGTfKU2LHNMvFIsvatgYlSucShDPI8uoeJ_G31uYPke-LJlRy-WVIhkudDg" + "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtrdU5tSmF0ZUNUZXI1V0JycUhCVUM0YUM3TjlOV1NyTUR" + + "KNmVkQXY1V0NmMiIsInN1YiI6ImRpZDprZXk6ejZNa2t1Tm1KYXRlQ1RlcjVXQnJxSEJVQzRhQzdOOU5XU3JNREo2Z" + + "WRBdjVXQ2YyIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjE" + + "iXSwiaWQiOiIxNjk4NDIyNDAxMzUyIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlNhbmN0aW9uc0NyZ" + + "WRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1ra3VObUphdGVDVGVyNVdCcnFIQlVDNGFDN045TldTck1ESjZ" + + "lZEF2NVdDZjIiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTEwLTI3VDE2OjAwOjAxWiIsImNyZWRlbnRpYWxTdWJqZWN0I" + + "jp7ImlkIjoiZGlkOmtleTp6Nk1ra3VObUphdGVDVGVyNVdCcnFIQlVDNGFDN045TldTck1ESjZlZEF2NVdDZjIiLCJ" + + "iZWVwIjoiYm9vcCJ9fX0.Xhd9nDdkGarYFr6FP7wqsgj5CK3oGTfKU2LHNMvFIsvatgYlSucShDPI8uoeJ_G31uYPk" + + "e-LJlRy-WVIhkudDg" private fun readPd(path: String): String { diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 3a0d6da16..8f5e29533 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -17,16 +17,15 @@ import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.Did +import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.extensions.load -import web5.sdk.dids.methods.ion.CreateDidIonOptions -import web5.sdk.dids.methods.ion.DidIon -import web5.sdk.dids.methods.ion.JsonWebKey2020VerificationMethod +import web5.sdk.dids.methods.dht.CreateDidDhtOptions +import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors import java.io.File import java.security.SignatureException import java.text.ParseException -import java.util.UUID import kotlin.test.Ignore import kotlin.test.assertEquals import kotlin.test.assertFails @@ -48,7 +47,7 @@ class VerifiableCredentialTest { "2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNsaVVIbHBQQjE0VVpkVzk4S250aG8zV2YxRjQxOU83cFhSMGhPeFAzRkNnIn0" + "sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEU2FMNHZVNElzNmxDalp4YVp6Zl9lWFFMU3V5T3E5T0pNbVJHa2FFTzRCQSIsInJlY29" + "2ZXJ5Q29tbWl0bWVudCI6IkVpQzI0TFljVEdRN1JzaDdIRUl2TXQ0MGNGbmNhZGZReTdibDNoa3k0RkxUQ2cifX0" - val issuerDid = DidIon.load(didUri, keyManager) + val issuerDid = DidDht.load(didUri, keyManager) val holderDid = DidKey.create(keyManager) val vc = VerifiableCredential.create( @@ -120,23 +119,24 @@ class VerifiableCredentialTest { fun `verify handles DIDs without an assertionMethod`() { val keyManager = InMemoryKeyManager() - //Create an ION DID without an assertionMethod + // Create a DHT DID without an assertionMethod val alias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) val verificationJwk = keyManager.getPublicKey(alias) - val key = JsonWebKey2020VerificationMethod( - id = UUID.randomUUID().toString(), - publicKeyJwk = verificationJwk, - relationships = emptyList() //No assertionMethod - ) - val issuerDid = DidIon.create( + + val verificationMethodsToAdd = listOf(Triple( + verificationJwk, + emptyList(), + "did:web:tbd.website" + )) + val issuerDid = DidDht.create( InMemoryKeyManager(), - CreateDidIonOptions(verificationMethodsToAdd = listOf(key)) + CreateDidDhtOptions(verificationMethods = verificationMethodsToAdd) ) val header = JWSHeader.Builder(JWSAlgorithm.ES256K) .keyID(issuerDid.uri) .build() - //A detached payload JWT + // A detached payload JWT val vcJwt = "${header.toBase64URL()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { @@ -247,6 +247,7 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> + println(vector.description) val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) val keyManager = InMemoryKeyManager() @@ -270,6 +271,7 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/verify.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> + println(vector.description) assertDoesNotThrow { VerifiableCredential.verify(vector.input.vcJwt) } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 1c8ba92d1..42ffa8ff2 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -15,13 +15,12 @@ import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.methods.ion.CreateDidIonOptions -import web5.sdk.dids.methods.ion.DidIon -import web5.sdk.dids.methods.ion.JsonWebKey2020VerificationMethod +import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.methods.dht.CreateDidDhtOptions +import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey import java.security.SignatureException import java.text.ParseException -import java.util.UUID import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -228,17 +227,17 @@ class VerifiablePresentationTest { fun `verify throws exception for DIDs without an assertionMethod`() { val keyManager = InMemoryKeyManager() - //Create an ION DID without an assertionMethod + //Create a DHT DID without an assertionMethod val alias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) val verificationJwk = keyManager.getPublicKey(alias) - val key = JsonWebKey2020VerificationMethod( - id = UUID.randomUUID().toString(), - publicKeyJwk = verificationJwk, - relationships = emptyList() //No assertionMethod - ) - val issuerDid = DidIon.create( + val verificationMethodsToAdd = listOf(Triple( + verificationJwk, + emptyList(), + "did:web:tbd.website" + )) + val issuerDid = DidDht.create( InMemoryKeyManager(), - CreateDidIonOptions(verificationMethodsToAdd = listOf(key)) + CreateDidDhtOptions(verificationMethods = verificationMethodsToAdd) ) val header = JWSHeader.Builder(JWSAlgorithm.ES256K) diff --git a/dids/build.gradle.kts b/dids/build.gradle.kts index 2fa65ba34..ec29e0689 100644 --- a/dids/build.gradle.kts +++ b/dids/build.gradle.kts @@ -28,13 +28,6 @@ dependencies { * Deps are declared in alphabetical order. */ - // API - /* - * API Leak: https://github.com/TBD54566975/web5-kt/issues/231 - * - * Change and move to "implementation" when completed - */ - api(libs.decentralizedIdentityDidCommonJava) // Project implementation(project(":common")) diff --git a/dids/module.md b/dids/module.md index 9d13b72d2..4afa685ac 100644 --- a/dids/module.md +++ b/dids/module.md @@ -31,7 +31,7 @@ import foundation.identity.did.Service val keyManager = InMemoryKeyManager() // Add a service to the DID Document -val service = Service.builder() +val service = Service.Builder() .id(URI("test-service")) .type("HubService") .serviceEndpoint("https://example.com/service)") @@ -54,114 +54,6 @@ val did = DidDht.create(keyManager, opts) val did = DidDht.resolve("did:dht:gb46emk73wkenrut43ii67a3o5qctojcaucebth7r83pst6yeh8o") ``` -# Package web5.sdk.dids.methods.ion - -Package that contains the `DidIon` class, which is used to create and resolve dids using the `ion` method. - -# Examples - -## Creation - -### Create an ION did - -This is the simplest way to create an ion did. - -```kotlin -val did = DidIon.create(InMemoryKeyManager()) -``` - -The private keys will be stored in the `InMemoryKeyManager`. All the defaults are used for -the `DidIonApi`, including the endpoint for the ION node used for creation, and uses -`OkHttp` as the `HttpClientEngine` (see [ktor engines](https://ktor.io/docs/http-client-engines.html)). - -### Create an ION did with custom ION endpoint and engine - -```kotlin -val keyManager = InMemoryKeyManager() -val ionApi = DidIonApi { - ionHost = "my_custom_ion_host" - engine = OkHttp.create { - preconfigured = OkHttpClient.Builder() - .connectTimeout(Duration.ofSeconds(4)) - .build() - } -} -val did = ionApi.create(keyManager) -``` - -### Create an ION did with custom creation options - -This is considered an advanced use case. - -Make sure that you have access to the private keys associated with the public keys you're passing in -(i.e. `verification`, `update`, and `recovery` keys). You can generate with the `keyManager`, or -store them elsewhere however you see fit. - -```kotlin -val keyManager = InMemoryKeyManager() -val opts = CreateDidIonOptions( - verificationMethodsToAdd = listOf( - VerificationMethodCreationParams( - JWSAlgorithm.ES256K, - relationships = listOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD) - ), - VerificationMethodCreationParams( - JWSAlgorithm.ES256K, - relationships = listOf(PublicKeyPurpose.ASSERTION_METHOD) - ), - ) - val did = DidIon . create (keyManager, opts) -``` - -## Resolution - -### Resolve an ION did - -```kotlin -val didResolutionResult = DidIon.resolve("did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w") -``` - -## DID ION Operations - -### Recover an ION did - -Any sidetree based DID, including ION, supports -a [recover operation](https://identity.foundation/sidetree/spec/#recover). -This type of operation is useful when the update keys of your DID have been compromised. - -```kotlin -// We create the DID first. -val keyManager = InMemoryKeyManager() -val did = DidIon.create(keyManager) -val recoveryKeyAlias = did.creationMetadata!!.keyAliases.verificationKeyAlias - -// Imagine that your update key was compromised, so you need to recover your DID. -val opts = RecoverDidIonOptions( - recoveryKeyAlias = recoveryKeyAlias.first(), -) -val recoverResult = DidIon.recover(keyManager, did.uri, opts) -``` - -**NOTE**: The `keyManager` MUST contain the recovery private key. - -### Deactivate an ION did - -```kotlin -// We create the DID first. -val ionApi = DidIon -val keyManager = InMemoryKeyManager() -val did = ionApi.create(keyManager) -val recoveryKeyAlias = did.creationMetadata!!.keyAliases.verificationKeyAlias.first() - -// You want to permanently disable the DID, rendering it useless. -val opts = DeactivateDidIonOptions( - recoveryKeyAlias = recoveryKeyAlias, -) -val deactivateResult = ionApi.deactivate(keyManager, did.uri, opts) -``` - -**NOTE**: The `keyManager` MUST contain the recovery private key. - # Package web5.sdk.dids.methods.key Package that contains the `DidKey` class, which is used to create and resolve dids using the `key` method. diff --git a/dids/src/intTest/kotlin/web5/sdk/dids/DidIntegrationTest.kt b/dids/src/intTest/kotlin/web5/sdk/dids/DidIntegrationTest.kt deleted file mode 100644 index b21078700..000000000 --- a/dids/src/intTest/kotlin/web5/sdk/dids/DidIntegrationTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package web5.sdk.dids - -import org.junit.jupiter.api.Test -import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.methods.ion.DidIon -import web5.sdk.dids.methods.web.DidWeb -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class DidIntegrationTest { - @Test - fun `create ion did over network`() { - val did = DidIon.create(InMemoryKeyManager()) - assertContains(did.uri, "did:ion:") - assertTrue(did.creationMetadata!!.longFormDid.startsWith(did.uri)) - } - - @Test - fun `resolve an existing web did`() { - val did = DidWeb.resolve("did:web:www.linkedin.com") - assertEquals("did:web:www.linkedin.com", did.didDocument!!.id.toString()) - } -} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index b32ae2664..42d534c64 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -1,11 +1,8 @@ package web5.sdk.dids -import com.nimbusds.jose.jwk.JWK -import foundation.identity.did.DID -import foundation.identity.did.DIDDocument -import foundation.identity.did.VerificationMethod import web5.sdk.crypto.KeyManager -import java.security.SignatureException +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.exceptions.PublicKeyJwkMissingException /** * A base abstraction for Decentralized Identifiers (DID) compliant with the W3C DID standard. @@ -171,35 +168,16 @@ public interface DidMethod { public fun load(uri: String, keyManager: KeyManager): T } -/** - * Finds the first available assertion method from the [DIDDocument]. When [assertionMethodId] - * is null, the function will return the first available assertion method. - */ -@JvmOverloads -public fun DIDDocument.findAssertionMethodById(assertionMethodId: String? = null): VerificationMethod { - require(!assertionMethodVerificationMethodsDereferenced.isNullOrEmpty()) { - throw SignatureException("No assertion methods found in DID document") - } - - val assertionMethod: VerificationMethod = when { - assertionMethodId != null -> assertionMethodVerificationMethodsDereferenced.find { - it.id.toString() == assertionMethodId - } - - else -> assertionMethodVerificationMethodsDereferenced.firstOrNull() - } ?: throw SignatureException("assertion method \"$assertionMethodId\" not found") - return assertionMethod -} internal fun DidMethod.validateKeyMaterialInsideKeyManager( did: String, keyManager: KeyManager) { - require(DID.fromString(did).methodName == methodName) { + require(DidUri.parse(did).method == methodName) { "did must start with the prefix \"did:$methodName\", but got $did" } val didResolutionResult = resolve(did) - didResolutionResult.didDocument!!.allVerificationMethods.forEach { - val publicKeyJwk = JWK.parse(it.publicKeyJwk) + didResolutionResult.didDocument!!.verificationMethod?.forEach { + val publicKeyJwk = it.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") val keyAlias = keyManager.getDeterministicAlias(publicKeyJwk) keyManager.getPublicKey(keyAlias) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt b/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt index c8b330ce9..6b7a9e53b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import foundation.identity.did.DIDDocument -import web5.sdk.dids.methods.ion.models.MetadataMethod +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DIDDocumentMetadata import java.util.Objects.hash /** @@ -21,7 +21,7 @@ public class DidResolutionResult( @JsonProperty("@context") public val context: String? = null, public val didDocument: DIDDocument? = null, - public val didDocumentMetadata: DidDocumentMetadata = DidDocumentMetadata(), + public val didDocumentMetadata: DIDDocumentMetadata = DIDDocumentMetadata(), public val didResolutionMetadata: DidResolutionMetadata = DidResolutionMetadata(), ) { override fun toString(): String { @@ -71,29 +71,4 @@ public class DidResolutionMetadata( public var additionalProperties: MutableMap? = null, ) -/** - * Contains metadata about the DID document. - * - * @property created Timestamp of when the DID was created. - * @property updated Timestamp of the last time the DID was updated. - * @property deactivated Indicates whether the DID has been deactivated. `true` if deactivated, `false` otherwise. - * @property versionId Specific version of the DID document. - * @property nextUpdate Timestamp of the next expected update of the DID document. - * @property nextVersionId The version ID expected for the next version of the DID document. - * @property equivalentId Alternative ID that can be used interchangeably with the canonical DID. - * @property canonicalId The canonical ID of the DID as per method-specific rules. - * @property types Returns types for DIDs that support type indexing. - */ -public class DidDocumentMetadata( - public var created: String? = null, - public var updated: String? = null, - public var deactivated: Boolean? = null, - public var versionId: String? = null, - public var nextUpdate: String? = null, - public var nextVersionId: String? = null, - public var equivalentId: List? = null, - public var canonicalId: String? = null, - public val method: MetadataMethod? = null, - public val types: List? = null -) diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt index 26801d51d..274384a53 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt @@ -1,6 +1,6 @@ package web5.sdk.dids -import foundation.identity.did.DID +import web5.sdk.dids.didcore.DidUri import web5.sdk.dids.extensions.supportedMethods /** @@ -32,9 +32,9 @@ public object DidResolvers { * @throws IllegalArgumentException if resolving the specified DID method is not supported. */ public fun resolve(didUrl: String, options: ResolveDidOptions? = null): DidResolutionResult { - val parsedDid = DID.fromString(didUrl) - val resolver = methodResolvers.getOrElse(parsedDid.methodName) { - throw IllegalArgumentException("Resolving did:${parsedDid.methodName} not supported") + val parsedDidUri = DidUri.parse(didUrl) + val resolver = methodResolvers.getOrElse(parsedDidUri.method) { + throw IllegalArgumentException("Resolving did:${parsedDidUri.method} not supported") } return resolver(didUrl, options) diff --git a/dids/src/main/kotlin/web5/sdk/dids/Models.kt b/dids/src/main/kotlin/web5/sdk/dids/Models.kt deleted file mode 100644 index 8ebc9da94..000000000 --- a/dids/src/main/kotlin/web5/sdk/dids/Models.kt +++ /dev/null @@ -1,14 +0,0 @@ -package web5.sdk.dids - -import com.fasterxml.jackson.annotation.JsonValue - -/** - * Enum representing the purpose of a public key. - */ -public enum class PublicKeyPurpose(@get:JsonValue public val code: String) { - AUTHENTICATION("authentication"), - KEY_AGREEMENT("keyAgreement"), - ASSERTION_METHOD("assertionMethod"), - CAPABILITY_DELEGATION("capabilityDelegation"), - CAPABILITY_INVOCATION("capabilityInvocation"), -} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt new file mode 100644 index 000000000..1de8ae552 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt @@ -0,0 +1,51 @@ +package web5.sdk.dids + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.nimbusds.jose.jwk.JWK +import web5.sdk.dids.didcore.Purpose +import java.io.IOException + +/** + * Serialize JWK into String. + */ +public class JWKSerializer : JsonSerializer() { + public override fun serialize(jwk: JWK?, gen: JsonGenerator, serializers: SerializerProvider?) { + val jwkString = jwk?.toJSONString() + + gen.writeRawValue(jwkString) + } + +} + +/** + * Deserialize String into JWK. + * + */ +public class JwkDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): JWK { + val node = p.codec.readTree(p) + val jwkJson = node.toString() + return JWK.parse(jwkJson) + } +} + +/** + * Deserialize String into List of Purpose enums. + * + */ +public class PurposesDeserializer : JsonDeserializer>() { + @Throws(IOException::class, JsonProcessingException::class) + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): List { + val node: JsonNode = p.codec.readTree(p) + return node.mapNotNull { jsonNode -> + Purpose.fromValue(jsonNode.asText()) + } + } +} diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDCore.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDCore.kt new file mode 100644 index 000000000..1ecbac84c --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDCore.kt @@ -0,0 +1,37 @@ +package web5.sdk.dids.didcore + +/** + * VerificationMethod Selector. + */ +public interface VMSelector + +/** + * ID. + * @property value The value of the ID + */ +public class ID(public val value: String) : VMSelector + + +/** + * Enum representing the purpose of a public key. + */ +public enum class Purpose(public val value: String) : VMSelector { + AssertionMethod("assertionMethod"), + Authentication("authentication"), + CapabilityDelegation("capabilityDelegation"), + CapabilityInvocation("capabilityInvocation"), + KeyAgreement("keyAgreement"); + + public companion object { + private val map = entries.associateBy(Purpose::value) + + /** + * Retrieve Purpose enum from String value. + * + * @param value of the purpose + * @return Purpose enum + */ + public fun fromValue(value: String): Purpose? = map[value] + } +} + diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt new file mode 100644 index 000000000..6c1f5c2cf --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt @@ -0,0 +1,296 @@ +package web5.sdk.dids.didcore + +import com.fasterxml.jackson.annotation.JsonProperty +import java.security.SignatureException + +/** + * DIDDocument represents 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. + * DID Core spec: https://www.w3.org/TR/did-core/#core-properties + * + * @property id the DID URI for a particular DID subject, expressed using the id property in the DID document. + * @property context a list of URI that defines the schema version used in the document. + * @property alsoKnownAs AlsoKnownAs can contain multiple identifiers for different purposes, + * or at different times for the same DID subject. 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. + * @property controller defines 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. + * It can be a string or a list of strings. + * @property verificationMethod a list of cryptographic public keys, which can be used to authenticate or authorize + * interactions with the DID subject or associated parties. + * @property service expresses ways of communicating with the DID subject or associated entities. + * A service can be any type of service the DID subject wants to advertise. + * @property assertionMethod used to specify how the DID subject is expected to express claims, + * such as for the purposes of issuing a Verifiable Credential. + * @property authentication specifies 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. + * @property keyAgreement specifies how an entity can generate encryption material to transmit confidential + * information intended for the DID subject, such as for establishing a secure communication channel. + * @property capabilityDelegation specifies a mechanism used by the DID subject to delegate a + * cryptographic capability to another party, such as delegating the authority to access a specific HTTP API. + * @property capabilityInvocation specifies a verification method used by the DID subject to invoke a + * cryptographic capability, such as the authorization to update the DID Document. + */ +public class DIDDocument( + public val id: String, + @JsonProperty("@context") + public val context: List? = null, + public val alsoKnownAs: List? = null, + public val controller: List? = null, + public val verificationMethod: List? = null, + public val service: List? = null, + public val assertionMethod: List? = null, + public val authentication: List? = null, + public val keyAgreement: List? = null, + public val capabilityDelegation: List? = null, + public val capabilityInvocation: List? = null +) { + + /** + * Select verification method takes a selector that can be used to select a specific verification + * method from the DID Document. If a selector is not provided, the first verification method + * is returned + * + * The selector can either be an ID, Purpose, or null. If a Purpose is provided, the first verification + * method in the DID Document that has the provided purpose will be returned. + * + * @param selector can either be an ID, Purpose, or null + * @return VerificationMethod matching the selector criteria + */ + public fun selectVerificationMethod(selector: VMSelector?): VerificationMethod { + if (verificationMethod.isNullOrEmpty()) throw Exception("No verification methods found") + + if (selector == null) return verificationMethod.first() + + val vmID = when (selector) { + is Purpose -> { + val purposeList = when (selector) { + Purpose.AssertionMethod -> assertionMethod + Purpose.Authentication -> authentication + Purpose.CapabilityDelegation -> capabilityDelegation + Purpose.CapabilityInvocation -> capabilityInvocation + Purpose.KeyAgreement -> keyAgreement + } + purposeList?.firstOrNull() + ?: throw Exception("No verification method found for purpose: ${selector.name}") + } + + is ID -> selector.value + else -> throw Exception("Invalid selector type $selector") + } + + val vm = this.verificationMethod.find { it.id == vmID } + ?: throw Exception("No verification method found for id: $vmID") + return vm + } + + /** + * GetAbsoluteResourceID returns a fully qualified ID for a document resource (e.g. service, verification method) + * Document Resource IDs are allowed to be relative DID URLs as a means to reduce storage size of DID Documents. + * More info here: https://www.w3.org/TR/did-core/#relative-did-urls + * + * @param id of the resource + * @return fully qualified ID for a document resource + */ + public fun getAbsoluteResourceID(id: String): String { + return if (id.startsWith("#")) "${this.id}$id" else id + } + + /** + * Finds the first available assertion method from the [DIDDocument]. When [assertionMethodId] + * is null, the function will return the first available assertion method. + * + * @param assertionMethodId The ID of the assertion method to be found + * @return VerificationMethod with purpose of Assertion + */ + @JvmOverloads + public fun findAssertionMethodById(assertionMethodId: String? = null): VerificationMethod { + require(!assertionMethod.isNullOrEmpty()) { + throw SignatureException("No assertion methods found in DID document") + } + + if (assertionMethodId != null) { + require(assertionMethod.contains(assertionMethodId)) { + throw SignatureException("assertion method \"$assertionMethodId\" not found in list of assertion methods") + } + } + + val assertionMethod: VerificationMethod = + verificationMethod + ?.find { + it.id == (assertionMethodId ?: assertionMethod.first()) + } + ?: throw SignatureException("assertion method \"$assertionMethodId\" not found") + + return assertionMethod + } + + /** + * Builder object to build a DIDDocument. + */ + public class Builder { + + private var id: String? = null + private var context: List? = null + private var alsoKnownAs: List? = null + private var controller: List? = null + + private var verificationMethod: MutableSet? = null + private var service: List? = null + + private var assertionMethod: MutableList? = null + private var authenticationMethod: MutableList? = null + private var keyAgreementMethod: MutableList? = null + private var capabilityDelegationMethod: MutableList? = null + private var capabilityInvocationMethod: MutableList? = null + + /** + * Adds Id to the DIDDocument. + * + * @param id of the DIDDocument + * @return Builder object + */ + public fun id(id: String): Builder = apply { this.id = id } + + /** + * Sets Context to the DIDDocument. + * + * @param context of the DIDDocument + * @return Builder object + */ + public fun context(context: List): Builder = apply { + this.context = context + } + + /** + * Sets Controllers. + * + * @param controllers to be set on the DIDDocument + * @return Builder object + */ + public fun controllers(controllers: List): Builder = apply { this.controller = controllers } + + /** + * Sets AlsoknownAses. + * + * @param alsoKnownAses to be set on the DIDDocument + * @return Builder object + */ + public fun alsoKnownAses(alsoKnownAses: List): Builder = apply { this.alsoKnownAs = alsoKnownAses } + + /** + * Sets Services. + * + * @param services to be set on the DIDDocument + * @return Builder object + */ + public fun services(services: List?): Builder = apply { this.service = services } + + /** + * Add verification method adds a verification method to the document. + * If Purposes are provided, the verification method's ID will be added to the corresponding list of purposes. + * + * @param method VerificationMethod to be added to the document + * @param purposes List of purposes to which the verification method will be added + */ + @JvmOverloads + public fun verificationMethodForPurposes( + method: VerificationMethod, + purposes: List = emptyList()): Builder = + apply { + this.verificationMethod = (this.verificationMethod ?: mutableSetOf()).apply { add(method) } + purposes.forEach { purpose -> + when (purpose) { + Purpose.AssertionMethod -> this.assertionMethod = + (this.assertionMethod ?: mutableListOf()).apply { add(method.id) } + + Purpose.Authentication -> this.authenticationMethod = + (this.authenticationMethod ?: mutableListOf()).apply { add(method.id) } + + Purpose.KeyAgreement -> this.keyAgreementMethod = + (this.keyAgreementMethod ?: mutableListOf()).apply { add(method.id) } + + Purpose.CapabilityDelegation -> this.capabilityDelegationMethod = + (this.capabilityDelegationMethod ?: mutableListOf()).apply { add(method.id) } + + Purpose.CapabilityInvocation -> this.capabilityInvocationMethod = + (this.capabilityInvocationMethod ?: mutableListOf()).apply { add(method.id) } + } + } + } + + /** + * Adds VerificationMethods for a single purpose. + * + * @param methodIds a list of VerificationMethodIds to be added to the DIDDocument + * @param purpose a single purpose to be associated with the list of VerificationMethods + * @return Builder object + */ + public fun verificationMethodIdsForPurpose( + methodIds: MutableList?, + purpose: Purpose): Builder = + apply { + methodIds?.forEach { id -> + when (purpose) { + Purpose.AssertionMethod -> this.assertionMethod = + (this.assertionMethod ?: mutableListOf()).apply { add(id) } + + Purpose.Authentication -> this.authenticationMethod = + (this.authenticationMethod ?: mutableListOf()).apply { add(id) } + + Purpose.KeyAgreement -> this.keyAgreementMethod = + (this.keyAgreementMethod ?: mutableListOf()).apply { add(id) } + + Purpose.CapabilityDelegation -> this.capabilityDelegationMethod = + (this.capabilityDelegationMethod ?: mutableListOf()).apply { add(id) } + + Purpose.CapabilityInvocation -> this.capabilityInvocationMethod = + (this.capabilityInvocationMethod ?: mutableListOf()).apply { add(id) } + } + } + } + + /** + * Builds DIDDocument after validating the required fields. + * + * @return DIDDocument + */ + public fun build(): DIDDocument { + check(id != null) { "ID is required" } + return DIDDocument( + id!!, + context, + alsoKnownAs, + controller, + verificationMethod?.toList(), + service, + assertionMethod, + authenticationMethod, + keyAgreementMethod, + capabilityDelegationMethod, + capabilityInvocationMethod + ) + } + } + + override fun toString(): String { + return "DIDDocument(" + + "id='$id', " + + "context='$context', " + + "alsoKnownAs=$alsoKnownAs, " + + "controller=$controller, " + + "verificationMethod=$verificationMethod, " + + "service=$service, " + + "assertionMethod=$assertionMethod, " + + "authentication=$authentication, " + + "keyAgreement=$keyAgreement, " + + "capabilityDelegation=$capabilityDelegation, " + + "capabilityInvocation=$capabilityInvocation)" + } +} + diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt new file mode 100644 index 000000000..301812f26 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt @@ -0,0 +1,24 @@ +package web5.sdk.dids.didcore + +/** + * Contains metadata about the DID document. + * DID document metadata spec: https://www.w3.org/TR/did-core/#did-document-metadata + * @property created Timestamp of when the DID was created. + * @property updated Timestamp of the last time the DID was updated. + * @property deactivated Indicates whether the DID has been deactivated. `true` if deactivated, `false` otherwise. + * @property versionId Specific version of the DID document. + * @property nextUpdate Timestamp of the next expected update of the DID document. + * @property nextVersionId The version ID expected for the next version of the DID document. + * @property equivalentId Alternative ID that can be used interchangeably with the canonical DID. + * @property canonicalId The canonical ID of the DID as per method-specific rules. + */ +public open class DIDDocumentMetadata( + public var created: String? = null, + public var updated: String? = null, + public var deactivated: Boolean? = null, + public var versionId: String? = null, + public var nextUpdate: String? = null, + public var nextVersionId: String? = null, + public var equivalentId: String? = null, + public var canonicalId: String? = null, +) diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt new file mode 100644 index 000000000..7b13cdc2f --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt @@ -0,0 +1,126 @@ +package web5.sdk.dids.didcore + +import web5.sdk.dids.exceptions.ParserException +import java.util.regex.Pattern + +/** + * DID provides a way to parse and handle Decentralized Identifier (DID) URIs + * according to the W3C DID Core specification (https://www.w3.org/TR/did-core/). + * + * @property uri represents the complete Decentralized Identifier (DID) URI. + * Spec: https://www.w3.org/TR/did-core/#did-syntax + * @property url represents the DID URI + A network location identifier for a specific resource + * Spec: https://www.w3.org/TR/did-core/#did-url-syntax + * @property method specifies the DID method in the URI, which indicates the underlying + * method-specific identifier scheme (e.g., jwk, dht, key, etc.). + * Spec: https://www.w3.org/TR/did-core/#method-schemes + * @property id is the method-specific identifier in the DID URI. + * Spec: https://www.w3.org/TR/did-core/#method-specific-id + * @property params is a map containing optional parameters present in the DID URI. + * These parameters are method-specific. + * Spec: https://www.w3.org/TR/did-core/#did-parameters + * @property path is an optional path component in the DID URI. + * Spec: https://www.w3.org/TR/did-core/#path + * @property query is an optional query component in the DID URI, used to express a request + * for a specific representation or resource related to the DID. + * Spec: https://www.w3.org/TR/did-core/#query + * @property fragment is an optional fragment component in the DID URI, used to reference + * a specific part of a DID document. + * Spec: https://www.w3.org/TR/did-core/#fragment + */ +public class DidUri( + public val uri: String, + public val url: String, + public val method: String, + public val id: String, + public val params: Map = emptyMap(), + public val path: String? = null, + public val query: String? = null, + public val fragment: String? = null +) { + override fun toString(): String { + return url + } + + /** + * Marshal text into byteArray. + * + * @return ByteArray of the DID object + */ + public fun marshalText(): ByteArray { + return this.toString().toByteArray(Charsets.UTF_8) + } + + /** + * Unmarshal byteArray into a DID. + * + * @param byteArray + * @return DID object + */ + public fun unmarshalText(byteArray: ByteArray): DidUri { + return parse(byteArray.toString(Charsets.UTF_8)) + } + + /** + * Parser object used to Parse a DID URI into a DID object. + */ + public companion object Parser { + private const val PCT_ENCODED_PATTERN = """(?:%[0-9a-fA-F]{2})""" + private const val ID_CHAR_PATTERN = """(?:[a-zA-Z0-9._-]|$PCT_ENCODED_PATTERN)""" + private const val METHOD_PATTERN = """([a-z0-9]+)""" + private const val METHOD_ID_PATTERN = """((?:$ID_CHAR_PATTERN*:)*($ID_CHAR_PATTERN+))""" + private const val PARAM_CHAR_PATTERN = """[a-zA-Z0-9_.:%-]""" + private const val PARAM_PATTERN = """;$PARAM_CHAR_PATTERN+=$PARAM_CHAR_PATTERN*""" + private const val PARAMS_PATTERN = """(($PARAM_PATTERN)*)""" + private const val PATH_PATTERN = """(/[^#?]*)?""" + private const val QUERY_PATTERN = """(\?[^\#]*)?""" + private const val FRAGMENT_PATTERN = """(\#.*)?""" + private val DID_URI_PATTERN = Pattern.compile( + "^did:$METHOD_PATTERN:$METHOD_ID_PATTERN$PARAMS_PATTERN$PATH_PATTERN$QUERY_PATTERN$FRAGMENT_PATTERN$" + ) + + /** + * Parse a DID URI into a DID object. + * + * @param didUri The DID URI to parse. + * @return DID object + */ + public fun parse(didUri: String): DidUri { + val matcher = DID_URI_PATTERN.matcher(didUri) + matcher.find() + if (!matcher.matches()) { + throw ParserException("Invalid DID URI") + } + + val method = matcher.group(1) + val id = matcher.group(2) + + val params = matcher.group(4) + ?.drop(1) + ?.split(";") + ?.mapNotNull { + it.split("=") + .takeIf { parts -> parts.size == 2 } + ?.let { parts -> parts[0] to parts[1] } + } + ?.takeIf { it.isNotEmpty() } + ?.associate { it } + ?: emptyMap() + + val path = matcher.group(6)?.takeIf { it.isNotEmpty() } + val query = matcher.group(7)?.drop(1) + val fragment = matcher.group(8)?.drop(1) + + return DidUri( + uri = "did:$method:$id", + url = didUri, + method = method, + id = id, + params = params, + path = path, + query = query, + fragment = fragment + ) + } + } +} diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/Service.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/Service.kt new file mode 100644 index 000000000..ab7f8332f --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/Service.kt @@ -0,0 +1,81 @@ +package web5.sdk.dids.didcore + +/** + * Service is 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. + * Service spec: https://www.w3.org/TR/did-core/#services + * + * @property id is the value of the id property and MUST be a URI conforming to RFC3986. + * A conforming producer MUST NOT produce multiple service entries with + * the same id. A conforming consumer MUST produce an error if it detects + * multiple service entries with the same id. + * @property type is an example of registered types which can be found + * here: https://www.w3.org/TR/did-spec-registries/#service-types + * @property serviceEndpoint is a network address, such as an HTTP URL, at which services + * operate on behalf of a DID subject. + */ +public class Service( + public val id: String, + public val type: String, + // todo: is serviceEndpoint a List or String for all DIDs + // did dht assumes this is List in diddht#fromDnsPacket + public val serviceEndpoint: List +) { + + override fun toString(): String { + return "Service(" + + "id='$id', " + + "type='$type', " + + "serviceEndpoint=$serviceEndpoint)" + } + + /** + * Builder object to build a Service. + */ + public class Builder { + private var id: String? = null + private var type: String? = null + private var serviceEndpoint: List? = null + + + /** + * Adds Id to the Service. + * + * @param id of the Service + * @return Builder object + */ + public fun id(id: String): Builder = apply { this.id = id } + + /** + * Adds Type to the Service. + * + * @param type of the Service + * @return Builder object + */ + public fun type(type: String): Builder = apply { this.type = type } + + /** + * Adds ServiceEndpoint to the Service. + * + * @param serviceEndpoint of the Service + * @return Builder object + */ + public fun serviceEndpoint(serviceEndpoint: List?): Builder = apply { + this.serviceEndpoint = serviceEndpoint + } + + /** + * Builds Service after validating the required fields. + * + * @return Service + */ + public fun build(): Service { + check(id != null) { "ID is required" } + check(type != null) { "Type is required" } + check(serviceEndpoint != null) { "ServiceEndpoint is required" } + return Service(id!!, type!!, serviceEndpoint!!) + } + + } +} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt new file mode 100644 index 000000000..d022b1a70 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt @@ -0,0 +1,108 @@ +package web5.sdk.dids.didcore + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.nimbusds.jose.jwk.JWK +import web5.sdk.dids.JWKSerializer +import web5.sdk.dids.JwkDeserializer + +/** + * VerificationMethod expresses 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 + * + * @property id id of the VerificationMethod + * @property type references exactly one verification method type. In order to maximize global + * interoperability, the verification method type SHOULD be registered in the + * DID Specification Registries: https://www.w3.org/TR/did-spec-registries/ + * @property controller a value that conforms to the rules in DID Syntax: https://www.w3.org/TR/did-core/#did-syntax + * @property publicKeyJwk specification reference: https://www.w3.org/TR/did-core/#dfn-publickeyjwk + */ +public class VerificationMethod( + public val id: String, + public val type: String, + public val controller: String, + @JsonSerialize(using = JWKSerializer::class) + @JsonDeserialize(using = JwkDeserializer::class) + public val publicKeyJwk: JWK? = null +) { + /** + * Checks type of VerificationMethod. + * + * @param type The type to check + * @return true/false if the type matches + */ + public fun isType(type: String): Boolean { + return type == this.type + } + + override fun toString(): String { + return "VerificationMethod(" + + "id='$id', " + + "type='$type', " + + "controller='$controller', " + + "publicKeyJwk=$publicKeyJwk)" + } + + /** + * Builder object to build a VerificationMethod. + */ + public class Builder { + private var id: String? = null + private var type: String? = null + private var controller: String? = null + private var publicKeyJwk: JWK? = null + + + /** + * Adds id to the VerificationMethod. + * + * @param id of the VerificationMethod + * @return Builder object + */ + public fun id(id: String): Builder = apply { this.id = id } + + /** + * Adds type to the VerificationMethod. + * + * @param type of the VerificationMethod + * @return Builder object + */ + public fun type(type: String): Builder = apply { this.type = type } + + /** + * Adds controller to the VerificationMethod. + * + * @param controller of the VerificationMethod + * @return Builder object + */ + public fun controller(controller: String): Builder = apply { this.controller = controller } + + /** + * Adds public key jwk to the VerificationMethod. + * + * @param publicKeyJwk of the VerificationMethod + * @return Builder object + */ + public fun publicKeyJwk(publicKeyJwk: JWK): Builder = apply { this.publicKeyJwk = publicKeyJwk } + + + /** + * Builds VerificationMethod after validating the required fields. + * + * @return VerificationMethod + */ + public fun build(): VerificationMethod { + check(id != null) { "ID is required" } + check(type != null) { "Type is required" } + check(controller != null) { "Controller is required" } + check(publicKeyJwk != null) { "PublicKeyJwk is required" } + return VerificationMethod(id!!, type!!, controller!!, publicKeyJwk!!) + } + + } +} diff --git a/dids/src/main/kotlin/web5/sdk/dids/exceptions/ExceptionDeclarations.kt b/dids/src/main/kotlin/web5/sdk/dids/exceptions/ExceptionDeclarations.kt index 7ee56dd31..90a0a0a7f 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/exceptions/ExceptionDeclarations.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/exceptions/ExceptionDeclarations.kt @@ -40,3 +40,17 @@ public class InvalidIdentifierException(message: String, cause: Throwable) : Run * @param message the exception message detailing the error */ public class DidResolutionException(message: String) : RuntimeException(message) + +/** + * Parser exception. + * + * @param message the exception message detailing the error + */ +public class ParserException(message: String) : RuntimeException(message) + +/** + * PublicKeyJwkMissingException. + * + * @param message the exception message detailing the error + */ +public class PublicKeyJwkMissingException(message: String) : RuntimeException(message) diff --git a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt b/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt index 29e84f812..36440b331 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt @@ -1,10 +1,9 @@ package web5.sdk.dids.extensions -import foundation.identity.did.DID import web5.sdk.crypto.KeyManager import web5.sdk.dids.Did +import web5.sdk.dids.didcore.DidUri import web5.sdk.dids.methods.dht.DidDht -import web5.sdk.dids.methods.ion.DidIon import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.dids.methods.key.DidKey import web5.sdk.dids.methods.web.DidWeb @@ -12,7 +11,6 @@ import web5.sdk.dids.methods.web.DidWeb internal val supportedMethods = mapOf( DidKey.methodName to DidKey.Companion, DidJwk.methodName to DidJwk.Companion, - DidIon.methodName to DidIon.Default, DidDht.methodName to DidDht.Default, DidWeb.methodName to DidWeb.Default ) @@ -23,5 +21,5 @@ internal val supportedMethods = mapOf( * to be used when the method of the DID is unknown. */ public fun Did.Companion.load(didUri: String, keyManager: KeyManager): Did { - return supportedMethods.getValue(DID.fromString(didUri).methodName).load(didUri, keyManager) + return supportedMethods.getValue(DidUri.parse(didUri).method).load(didUri, keyManager) } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt new file mode 100644 index 000000000..fb163ace5 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt @@ -0,0 +1,13 @@ +package web5.sdk.dids.methods.dht + +import web5.sdk.dids.didcore.DIDDocumentMetadata + +/** + * Did document metadata for did:dht that extends the base did document metadata. + * + * @property types list of types + * @constructor Create empty Did dht document metadata + */ +public class DIDDhtDocumentMetadata( + public val types: List? = null +) : DIDDocumentMetadata() diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt index a472e9595..a63331370 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt @@ -1,7 +1,6 @@ package web5.sdk.dids.methods.dht import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.OctetKeyPair import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -18,8 +17,6 @@ import org.xbill.DNS.DNSInput import org.xbill.DNS.Message import web5.sdk.common.ZBase32 import web5.sdk.crypto.Ed25519 -import web5.sdk.crypto.Jwa -import web5.sdk.crypto.JwaCurve import web5.sdk.crypto.KeyManager import web5.sdk.dids.exceptions.PkarrRecordNotFoundException import web5.sdk.dids.exceptions.PkarrRecordResponseException @@ -142,7 +139,7 @@ internal class DhtClient( // set the sequence number to the current time in seconds val seq = System.currentTimeMillis() / 1000 val v = message.toWire() - require(!v.isEmpty()) { + require(v.isNotEmpty()) { "Message must be not be empty" } return signBep44Message(manager, keyAlias, seq, v) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 5fd848fba..7fd485981 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -2,11 +2,6 @@ package web5.sdk.dids.methods.dht import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.JWK -import foundation.identity.did.DID -import foundation.identity.did.DIDDocument -import foundation.identity.did.Service -import foundation.identity.did.VerificationMethod -import foundation.identity.did.parser.ParserException import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -25,18 +20,22 @@ import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.Did -import web5.sdk.dids.DidDocumentMetadata import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.PublicKeyPurpose import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DIDDocumentMetadata +import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.didcore.Service +import web5.sdk.dids.didcore.VerificationMethod import web5.sdk.dids.exceptions.InvalidIdentifierException import web5.sdk.dids.exceptions.InvalidIdentifierSizeException import web5.sdk.dids.exceptions.InvalidMethodNameException import web5.sdk.dids.exceptions.PkarrRecordNotFoundException +import web5.sdk.dids.exceptions.PublicKeyJwkMissingException import web5.sdk.dids.validateKeyMaterialInsideKeyManager -import java.net.URI /** * Configuration for the [DidDhtApi]. @@ -91,7 +90,7 @@ private class DidDhtApiImpl(configuration: DidDhtConfiguration) : DidDhtApi(conf * @property alsoKnownAses A list of also known as identifiers to add to the DID Document. */ public class CreateDidDhtOptions( - public val verificationMethods: Iterable, String?>>? = null, + public val verificationMethods: Iterable, String?>>? = null, public val services: Iterable? = null, public val publish: Boolean = true, public val controllers: Iterable? = null, @@ -110,7 +109,7 @@ private val logger = KotlinLogging.logger {} public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod { private val engine: HttpClientEngine = configuration.engine - private val dht = DhtClient(configuration.gateway, engine) + private val dhtClient = DhtClient(configuration.gateway, engine) private val ttl: Long = 7200 override val methodName: String = "dht" @@ -139,73 +138,58 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod + requireNotNull(service.id) { "Service id cannot be null" } + requireNotNull(service.type) { "Service type cannot be null" } + requireNotNull(service.serviceEndpoint) { "Service serviceEndpoint cannot be null" } + + Service( + id = "$id#${service.id}", + type = service.type, + serviceEndpoint = service.serviceEndpoint + ) + } + + // build DID Document + val didDocumentBuilder = DIDDocument.Builder() + .id(id) + .services(services) + + opts.controllers?.let { didDocumentBuilder.controllers(it.toList()) } + opts.alsoKnownAses?.let { didDocumentBuilder.alsoKnownAses(it.toList()) } + // add identity key to relationships map val identityVerificationMethod = - VerificationMethod.builder() - .id(URI.create("$id#0")) - .type("JsonWebKey") - .controller(URI.create(id)) - .publicKeyJwk(publicKey.toPublicJWK().toJSONObject()) - .build() + VerificationMethod( + id = "$id#0", + type = "JsonWebKey", + controller = id, + publicKeyJwk = publicKey.toPublicJWK() + ) - // add all other keys to the verificationMethod and relationships arrays - val relationshipsMap = mutableMapOf>().apply { - val identityVerificationMethodRef = VerificationMethod.builder().id(identityVerificationMethod.id).build() + didDocumentBuilder.verificationMethodForPurposes( + identityVerificationMethod, listOf( - PublicKeyPurpose.AUTHENTICATION, - PublicKeyPurpose.ASSERTION_METHOD, - PublicKeyPurpose.CAPABILITY_DELEGATION, - PublicKeyPurpose.CAPABILITY_INVOCATION - ).forEach { purpose -> - getOrPut(purpose) { mutableListOf() }.add(identityVerificationMethodRef) - } - } + Purpose.AssertionMethod, + Purpose.Authentication, + Purpose.CapabilityDelegation, + Purpose.CapabilityInvocation + ) + ) - // map to the DID object model's verification methods - val verificationMethods = - listOf(identityVerificationMethod) + (opts.verificationMethods?.map { (key, purposes, controller) -> - VerificationMethod.builder() - .id(URI.create("$id#${key.keyID}")) - .type("JsonWebKey") - .controller(URI.create(controller ?: id)) - .publicKeyJwk(key.toPublicJWK().toJSONObject()) - .build().also { verificationMethod -> - purposes.forEach { relationship -> - relationshipsMap.getOrPut(relationship) { mutableListOf() }.add( - VerificationMethod.builder().id(verificationMethod.id).build() - ) - } - } - } ?: emptyList()) - opts.services?.forEach { service -> - requireNotNull(service.id) { "Service id cannot be null" } - requireNotNull(service.type) { "Service type cannot be null" } - requireNotNull(service.serviceEndpoint) { "Service serviceEndpoint cannot be null" } - } - // map to the DID object model's services - val services = opts.services?.map { service -> - Service.builder() - .id(URI.create("$id#${service.id}")) - .type(service.type) - .serviceEndpoint(service.serviceEndpoint) - .build() + opts.verificationMethods?.map { (key, purposes, controller) -> + VerificationMethod.Builder() + .id("$id#${key.keyID}") + .type("JsonWebKey") + .controller(controller ?: id) + .publicKeyJwk(key.toPublicJWK()) + .build().also { verificationMethod -> + didDocumentBuilder.verificationMethodForPurposes(verificationMethod, purposes.toList()) + + } } - // build DID Document - val didDocumentBuilder = - DIDDocument.builder() - .defaultContexts(false) - .id(URI(id)) - .verificationMethods(verificationMethods) - .services(services) - .assertionMethodVerificationMethods(relationshipsMap[PublicKeyPurpose.ASSERTION_METHOD]) - .authenticationVerificationMethods(relationshipsMap[PublicKeyPurpose.AUTHENTICATION]) - .keyAgreementVerificationMethods(relationshipsMap[PublicKeyPurpose.KEY_AGREEMENT]) - .capabilityDelegationVerificationMethods(relationshipsMap[PublicKeyPurpose.CAPABILITY_DELEGATION]) - .capabilityInvocationVerificationMethods(relationshipsMap[PublicKeyPurpose.CAPABILITY_INVOCATION]) - - opts.controllers?.let { didDocumentBuilder.controllers(it.map(URI::create)) } - opts.alsoKnownAses?.let { didDocumentBuilder.alsoKnownAses(it.map(URI::create)) } val didDocument = didDocumentBuilder.build() @@ -227,7 +211,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod return DidResolutionResult( didDocument = didDocument, - didDocumentMetadata = DidDocumentMetadata(types = types.map { it.index }) + didDocumentMetadata = DIDDhtDocumentMetadata(types = types.map { it.index }) ) } } @@ -274,11 +258,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod? = null) { - validate(didDocument.id.toString()) - val publishId = DidDht.suffix(didDocument.id.toString()) + validate(didDocument.id) + val publishId = DidDht.suffix(didDocument.id) + val dnsPacket = toDnsPacket(didDocument, types) val bep44Message = DhtClient.createBep44PutRequest(manager, getIdentityKid(didDocument), dnsPacket) - dht.pkarrPut(publishId, bep44Message) + dhtClient.pkarrPut(publishId, bep44Message) } /** @@ -299,9 +284,10 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod() // Add Resource Records for each Service - didDocument.services?.forEachIndexed { i, service -> + didDocument.service?.forEachIndexed { i, service -> val sId = "s$i" message.addRecord( TXTRecord( @@ -385,41 +373,41 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod().apply { if (verificationMethodIds.isNotEmpty()) add("vm=${verificationMethodIds.joinToString(ARRAY_SEPARATOR)}") if (serviceIds.isNotEmpty()) add("svc=${serviceIds.joinToString(ARRAY_SEPARATOR)}") - didDocument.authenticationVerificationMethodsDereferenced?.map { - verificationMethodsById[it.id.toString()] + didDocument.authentication?.map { + verificationMethodsById[it] }?.joinToString(ARRAY_SEPARATOR)?.let { add("auth=$it") } - didDocument.assertionMethodVerificationMethodsDereferenced?.map { - verificationMethodsById[it.id.toString()] + didDocument.assertionMethod?.map { + verificationMethodsById[it] }?.joinToString(ARRAY_SEPARATOR)?.let { add("asm=$it") } - didDocument.keyAgreementVerificationMethodsDereferenced?.map { - verificationMethodsById[it.id.toString()] + didDocument.keyAgreement?.map { + verificationMethodsById[it] }?.joinToString(ARRAY_SEPARATOR)?.let { add("agm=$it") } - didDocument.capabilityInvocationVerificationMethodsDereferenced?.map { - verificationMethodsById[it.id.toString()] + didDocument.capabilityInvocation?.map { + verificationMethodsById[it] }?.joinToString(ARRAY_SEPARATOR)?.let { add("inv=$it") } - didDocument.capabilityDelegationVerificationMethodsDereferenced?.map { - verificationMethodsById[it.id.toString()] + didDocument.capabilityDelegation?.map { + verificationMethodsById[it] }?.joinToString(ARRAY_SEPARATOR)?.let { add("del=$it") } } @@ -447,13 +435,13 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod, Map> { val verificationMethodsById = mutableMapOf() val verificationMethods = buildList { - didDocument.verificationMethods?.forEachIndexed { i, verificationMethod -> - val publicKeyJwk = JWK.parse(verificationMethod.publicKeyJwk) + didDocument.verificationMethod?.forEachIndexed { i, verificationMethod -> + val publicKeyJwk = verificationMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") val publicKeyBytes = Crypto.publicKeyToBytes(publicKeyJwk) val base64UrlEncodedKey = Convert(publicKeyBytes).toBase64Url(padding = false) val verificationMethodId = "k$i" - verificationMethodsById[verificationMethod.id.toString()] = verificationMethodId + verificationMethodsById[verificationMethod.id] = verificationMethodId val keyType = when (publicKeyJwk.algorithm) { JWSAlgorithm.EdDSA -> 0 @@ -468,11 +456,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod) { - endpoint.joinToString(ARRAY_SEPARATOR) - } else { - endpoint.toString() - } - return seValue - } - /** * Converts a DNS packet to a [DIDDocument] according to the did:dht spec * https://tbd54566975.github.io/did-dht-method/#dids-as-a-dns-packet @@ -532,10 +508,8 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod> { - val doc = DIDDocument.builder().id(URI.create(did)) - .defaultContexts(false) + val doc = DIDDocument.Builder().id(did) - val verificationMethods = mutableListOf() val services = mutableListOf() val types = mutableListOf() val keyLookup = mutableMapOf() @@ -546,17 +520,20 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod { - handleVerificationMethods(rr, verificationMethods, did, keyLookup, name) + handleVerificationMethods(rr, did, keyLookup, name, doc) } // handle services name.startsWith("_s") -> { val data = parseTxtData(rr.strings.joinToString(ARRAY_SEPARATOR)) - services += Service.builder() - .id(URI.create("$did#${data["id"]!!}")) + val service = Service.Builder() + .id(data["id"]!!) .type(data["t"]!!) .serviceEndpoint(data["se"]!!.split(ARRAY_SEPARATOR)) .build() + services.add(service) + doc.services(services) } // handle type indexing name.startsWith("_typ._did.") -> { @@ -585,29 +562,25 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod) { + private fun handleAlsoKnownAsRecord(rr: TXTRecord, doc: DIDDocument.Builder) { val data = rr.strings.joinToString("") - doc.alsoKnownAses(data.split(ARRAY_SEPARATOR).map { URI.create(it) }) + doc.alsoKnownAses(data.split(ARRAY_SEPARATOR)) } - private fun handleControllerRecord(rr: TXTRecord, doc: DIDDocument.Builder<*>) { + private fun handleControllerRecord(rr: TXTRecord, doc: DIDDocument.Builder) { val data = rr.strings.joinToString("") - doc.controllers(data.split(ARRAY_SEPARATOR).map { URI.create(it) }) + doc.controllers(data.split(ARRAY_SEPARATOR)) } private fun handleVerificationMethods( rr: TXTRecord, - verificationMethods: MutableList, did: String, keyLookup: MutableMap, - name: String + name: String, + didDocBuilder: DIDDocument.Builder ) { val data = parseTxtData(rr.strings.joinToString("")) val verificationMethodId = data["id"]!! @@ -620,25 +593,19 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod throw IllegalArgumentException("Unknown key type: ${data["t"]}") } - val builder = VerificationMethod.builder() - .id(URI.create("$did#$verificationMethodId")) + val vmBuilder = VerificationMethod.Builder() + .id("$did#$verificationMethodId") .type("JsonWebKey") - .publicKeyJwk(publicKeyJwk.toPublicJWK().toJSONObject()) + .publicKeyJwk(publicKeyJwk.toPublicJWK()) if (data.containsKey("c")) { - builder.controller(URI.create(data["c"]!!)) + vmBuilder.controller(data["c"]!!) } else { - builder.controller( - URI.create( - when (verificationMethodId) { - "0" -> did - else -> "" - } - ) - ) + vmBuilder.controller(did) } - verificationMethods += builder.build() + val vm = vmBuilder.build() + didDocBuilder.verificationMethodForPurposes(vm) keyLookup[name.split(".")[0].drop(1)] = "$did#$verificationMethodId" } @@ -646,13 +613,13 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod, - doc: DIDDocument.Builder<*> + doc: DIDDocument.Builder, ) { val rootData = rr.strings.joinToString(PROPERTY_SEPARATOR).split(PROPERTY_SEPARATOR) - val lists = mapOf( - "auth" to mutableListOf(), + val purposeToVmIds = mapOf>( "asm" to mutableListOf(), + "auth" to mutableListOf(), "agm" to mutableListOf(), "inv" to mutableListOf(), "del" to mutableListOf() @@ -660,19 +627,19 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod val (key, values) = item.split("=") - val valueItems = values.split(ARRAY_SEPARATOR) + val valuesList = values.split(ARRAY_SEPARATOR) - valueItems.forEach { - lists[key]?.add(VerificationMethod.builder().id(URI(keyLookup[it]!!)).build()) + valuesList.forEach { + purposeToVmIds[key]?.add(keyLookup[it]!!) } } - // add verification relationships - doc.authenticationVerificationMethods(lists["auth"]) - doc.assertionMethodVerificationMethods(lists["asm"]) - doc.keyAgreementVerificationMethods(lists["agm"]) - doc.capabilityInvocationVerificationMethods(lists["inv"]) - doc.capabilityDelegationVerificationMethods(lists["del"]) + // add vmIds to purpose lists + doc.verificationMethodIdsForPurpose(purposeToVmIds["asm"], Purpose.AssertionMethod) + doc.verificationMethodIdsForPurpose(purposeToVmIds["auth"], Purpose.Authentication) + doc.verificationMethodIdsForPurpose(purposeToVmIds["agm"], Purpose.KeyAgreement) + doc.verificationMethodIdsForPurpose(purposeToVmIds["del"], Purpose.CapabilityDelegation) + doc.verificationMethodIdsForPurpose(purposeToVmIds["inv"], Purpose.CapabilityInvocation) } /** diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/ion/DidIon.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/ion/DidIon.kt deleted file mode 100644 index 68e538d69..000000000 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/ion/DidIon.kt +++ /dev/null @@ -1,902 +0,0 @@ -package web5.sdk.dids.methods.ion - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSObject -import com.nimbusds.jose.Payload -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.util.Base64URL -import foundation.identity.did.DID -import io.ktor.client.HttpClient -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType -import io.ktor.http.contentType -import io.ktor.http.isSuccess -import io.ktor.serialization.jackson.jackson -import kotlinx.coroutines.runBlocking -import org.erdtman.jcs.JsonCanonicalizer -import web5.sdk.common.Convert -import web5.sdk.common.Varint -import web5.sdk.crypto.AlgorithmId -import web5.sdk.crypto.KeyGenOptions -import web5.sdk.crypto.KeyManager -import web5.sdk.dids.CreateDidOptions -import web5.sdk.dids.CreationMetadata -import web5.sdk.dids.Did -import web5.sdk.dids.DidMethod -import web5.sdk.dids.DidResolutionMetadata -import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.PublicKeyPurpose -import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.methods.ion.models.AddPublicKeysAction -import web5.sdk.dids.methods.ion.models.AddServicesAction -import web5.sdk.dids.methods.ion.models.Commitment -import web5.sdk.dids.methods.ion.models.DeactivateUpdateSignedData -import web5.sdk.dids.methods.ion.models.Delta -import web5.sdk.dids.methods.ion.models.Document -import web5.sdk.dids.methods.ion.models.InitialState -import web5.sdk.dids.methods.ion.models.OperationSuffixDataObject -import web5.sdk.dids.methods.ion.models.PatchAction -import web5.sdk.dids.methods.ion.models.PublicKey -import web5.sdk.dids.methods.ion.models.RecoveryUpdateSignedData -import web5.sdk.dids.methods.ion.models.RemovePublicKeysAction -import web5.sdk.dids.methods.ion.models.RemoveServicesAction -import web5.sdk.dids.methods.ion.models.ReplaceAction -import web5.sdk.dids.methods.ion.models.Reveal -import web5.sdk.dids.methods.ion.models.Service -import web5.sdk.dids.methods.ion.models.SidetreeCreateOperation -import web5.sdk.dids.methods.ion.models.SidetreeDeactivateOperation -import web5.sdk.dids.methods.ion.models.SidetreeRecoverOperation -import web5.sdk.dids.methods.ion.models.SidetreeUpdateOperation -import web5.sdk.dids.methods.ion.models.UpdateOperationSignedData -import web5.sdk.dids.validateKeyMaterialInsideKeyManager -import java.net.URI -import java.security.MessageDigest -import java.util.UUID - -private const val operationsPath = "/operations" -private const val identifiersPath = "/identifiers" - -private val sha256MultiCodec = Varint.encode(0x12) - -/** - * Configuration for the [DidIonApi]. - * - * @property ionHost The ION host URL. - * @property engine The engine to use. When absent, a new one will be created from the [OkHttp] factory. - */ -public class DidIonConfiguration internal constructor( - public var ionHost: String = "https://ion.tbddev.org", - public var engine: HttpClientEngine? = null, -) - - -/** - * Returns a [DidIonApi] after applying [configurationBlock] on the default [DidIonConfiguration]. - */ -public fun DidIonApi(configurationBlock: DidIonConfiguration.() -> Unit): DidIonApi { - val conf = DidIonConfiguration().apply(configurationBlock) - return DidIonApiImpl(conf) -} - -/** [DidIonApi] is sealed, so we provide an impl so the constructor can be called. */ -private class DidIonApiImpl(configuration: DidIonConfiguration) : DidIonApi(configuration) - -/** - * The options when updating an ION did. - * - * @param updateKeyAlias The alias within the key manager that refers to the last update key. - * @param servicesToAdd The services to add in the did document. - * @param idsOfServicesToRemove Ids of the services to remove from the did document. - * @param verificationMethodsToAdd List of specs that will be added to the DID ION document. - * @param idsOfPublicKeysToRemove Keys to remove from the DID document. - */ -public data class UpdateDidIonOptions( - val updateKeyAlias: String, - override val servicesToAdd: Iterable = emptyList(), - val idsOfServicesToRemove: Iterable = emptyList(), - override val verificationMethodsToAdd: Iterable = emptyList(), - val idsOfPublicKeysToRemove: Iterable = emptyList(), -) : CommonOptions { - internal fun toPatches(publicKeys: Iterable): List { - fun MutableList.addIfNotEmpty(iter: Iterable, action: (Iterable) -> PatchAction) { - iter.takeIf { it.count() != 0 }?.let { this.add(action(it)) } - } - - return buildList { - addIfNotEmpty(servicesToAdd, ::AddServicesAction) - addIfNotEmpty(idsOfServicesToRemove, ::RemoveServicesAction) - addIfNotEmpty(publicKeys, ::AddPublicKeysAction) - addIfNotEmpty(idsOfPublicKeysToRemove, ::RemovePublicKeysAction) - } - } -} - -/** - * The options when recovering an ION did. - * - * @param recoveryKeyAlias is the alias within the keyManager to use when signing. It must match the recovery used with - * the last recovery operation. - * @param verificationMethodsToAdd List of specs that will be added to the DID ION document. - * @param servicesToAdd When provided, the services will be added to the DID document. Note that for each of the - * services that should be added, the following must hold: - * - The `id` field cannot be over 50 chars and must only use characters from the Base64URL character set. - * - The `type` field cannot be over 30 characters. - * - The `serviceEndpoint` must be a valid URI. - */ -public class RecoverDidIonOptions( - public val recoveryKeyAlias: String, - public override val verificationMethodsToAdd: Iterable = emptyList(), - public override val servicesToAdd: Iterable = emptyList(), -) : CommonOptions - -/** - * Options when deactivating an ION did. - * - * [recoveryKeyAlias] is the alias within the keyManager to use when signing. It must match the recovery used with the - * last recovery operation. - */ -public class DeactivateDidIonOptions(public val recoveryKeyAlias: String) - - -/** - * Provides a specific implementation for creating and resolving "did:ion" method Decentralized Identifiers (DIDs). - * - * A "did:ion" DID is an implementation of the Sidetree protocol that uses Bitcoin as it's anchoring system. - * Further specifics and technical details are outlined in [the DID Sidetree Spec](https://identity.foundation/sidetree/spec/). - * - * @property uri The URI of the "did:ion" which conforms to the DID standard. - * @property keyManager A [KeyManager] instance utilized to manage the cryptographic keys associated with the DID. - * @property creationMetadata Metadata related to the creation of a DID. Useful for debugging purposes. - * @property didIonApi A [DidIonApi] instance utilized to delegate all the calls to an ION node. - */ -public class DidIon( - uri: String, - keyManager: KeyManager, - public val creationMetadata: IonCreationMetadata? = null, - private val didIonApi: DidIonApi) : Did(uri, keyManager) { - - /** - * Calls [DidIonApi.update] for this DID. - */ - public fun update(options: UpdateDidIonOptions): IonUpdateResult = didIonApi.update(keyManager, this.uri, options) - - /** - * Calls [DidIonApi.recover] for this DID. - */ - public fun recover(options: RecoverDidIonOptions): IonRecoverResult = didIonApi.recover(keyManager, this.uri, options) - - /** - * Calls [DidIonApi.deactivate] for this DID. - */ - public fun deactivate(options: DeactivateDidIonOptions): IonDeactivateResult = didIonApi.deactivate( - keyManager, - this.uri, - options - ) - - /** - * Calls [DidIonApi.resolve] for this DID. - */ - public fun resolve(options: ResolveDidOptions?): DidResolutionResult = didIonApi.resolve(uri, options) - - /** - * Default companion object for creating a [DidIonApi] with a default configuration. - */ - public companion object Default : DidIonApi(DidIonConfiguration()) -} - -private const val maxServiceTypeLength = 30 - -private const val maxIdLength = 50 - -private const val base64UrlCharsetRegexStr = "^[A-Za-z0-9_-]+$" - -private val base64UrlCharsetRegex = base64UrlCharsetRegexStr.toRegex() - -/** - * Base class for managing DID Ion operations. Uses the given [configuration]. - */ -public sealed class DidIonApi( - private val configuration: DidIonConfiguration -) : DidMethod { - - private val mapper = jacksonObjectMapper() - - private val operationsEndpoint = configuration.ionHost + operationsPath - private val identifiersEndpoint = configuration.ionHost + identifiersPath - - private val engine: HttpClientEngine = configuration.engine ?: OkHttp.create {} - - private val client = HttpClient(engine) { - install(ContentNegotiation) { - jackson { mapper } - } - } - - override val methodName: String = "ion" - - /** - * Creates a [DidIon], which includes a DID and it's associated DID Document. In order to ensure the creation - * works appropriately, the DID is resolved immediately after it's created. - * - * Note: [options] must be of type [CreateDidIonOptions]. - * @throws [ResolutionException] When there is an error after resolution. - * @throws [InvalidStatusException] When any of the network requests return an invalid HTTP status code. - * @see [DidMethod.create] for details of each parameter. - */ - override fun create(keyManager: KeyManager, options: CreateDidIonOptions?): DidIon { - val (createOp, keys) = createOperation(keyManager, options) - - val canonicalizedSuffixData = canonicalized(createOp.suffixData) - val suffixDataMultihash = multihash(canonicalizedSuffixData) - val shortFormDidSegment = Convert(suffixDataMultihash).toBase64Url(padding = false) - - val initialState = InitialState( - suffixData = createOp.suffixData, - delta = createOp.delta, - ) - val longFormDidSegment = didUriSegment(initialState) - - val response: HttpResponse = runBlocking { - client.post(operationsEndpoint) { - contentType(ContentType.Application.Json) - setBody(createOp) - } - } - - val opBody = runBlocking { - response.bodyAsText() - } - - if (response.status.isSuccess()) { - val shortFormDid = "did:ion:$shortFormDidSegment" - val longFormDid = "$shortFormDid:$longFormDidSegment" - val resolutionResult = resolve(longFormDid) - - if (!resolutionResult.didResolutionMetadata.error.isNullOrEmpty()) { - throw ResolutionException( - "error when resolving after creation: ${resolutionResult.didResolutionMetadata.error}" - ) - } - - return DidIon( - resolutionResult.didDocument!!.id.toString(), - keyManager, - IonCreationMetadata( - createOp, - shortFormDid, - longFormDid, - opBody, - keys - ), - this - ) - } - throw InvalidStatusException(response.status.value, "received error response: '$opBody'") - } - - /** - * Instantiates a [DidIon] instance from [uri] (which has to start with "did:ion:"), and validates that the - * associated key material exists in the provided [keyManager]. - * - * ### Usage Example: - * ```kotlin - * val keyManager = InMemoryKeyManager() - * val did = DidIon.load("did:ion:example", keyManager) - * ``` - */ - override fun load(uri: String, keyManager: KeyManager): DidIon { - validateKeyMaterialInsideKeyManager(uri, keyManager) - // TODO: validate other keys. - return DidIon(uri, keyManager, null, this) - } - - private fun canonicalized(data: Any): ByteArray { - val jsonString = mapper.writeValueAsString(data) - return JsonCanonicalizer(jsonString).encodedUTF8 - } - - private fun didUriSegment(initialState: InitialState): String { - val canonicalized = canonicalized(initialState) - return Convert(canonicalized).toBase64Url(padding = false) - } - - /** - * Given a [did], returns the [DidResolutionResult], which is specified in https://w3c-ccg.github.io/did-resolution/#did-resolution-result - * - * @throws [InvalidStatusException] When any of the network requests return an invalid HTTP status code. - */ - override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { - val didObj = DID.fromString(did) - require(didObj.methodName == methodName) { throw IllegalArgumentException("expected did:ion") } - - val resp = runBlocking { client.get("$identifiersEndpoint/$didObj") } - val body = runBlocking { resp.bodyAsText() } - if (!resp.status.isSuccess()) { - throw InvalidStatusException(resp.status.value, "resolution error response: '$body'") - } - return mapper.readValue(body, DidResolutionResult::class.java) - } - - /** - * Updates [did] with the given [options]. The update key must be available in the [keyManager]. - */ - public fun update(keyManager: KeyManager, did: String, options: UpdateDidIonOptions): IonUpdateResult { - val (updateOp, keyAliases) = createUpdateOperation(keyManager, did, options) - val response: HttpResponse = runBlocking { - client.post(operationsEndpoint) { - contentType(ContentType.Application.Json) - setBody(updateOp) - } - } - val opBody = runBlocking { response.bodyAsText() } - if (response.status.isSuccess()) { - return IonUpdateResult( - operationsResponseBody = opBody, - keyAliases = keyAliases, - ) - } - throw InvalidStatusException(response.status.value, "received error response: '$opBody'") - } - - private fun createUpdateOperation(keyManager: KeyManager, did: String, options: UpdateDidIonOptions): - Pair { - val parsedDid = DID.fromString(did) - require(!parsedDid.methodSpecificId.contains(":")) { - "updating a DID is only allowed for short form dids, but got $did" - } - val updatePublicKey = keyManager.getPublicKey(options.updateKeyAlias) - - val newUpdateKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val newUpdatePublicKey = keyManager.getPublicKey(newUpdateKeyAlias) - - val reveal = updatePublicKey.reveal() - val commitment = newUpdatePublicKey.commitment() - - validateServices(options.servicesToAdd) - - val publicKeysWithAliases = options.verificationMethodsToAdd.toPublicKeys(keyManager) - val publicKeys = publicKeysWithAliases.map { it.second } - validateDidDocumentKeys(publicKeys) - - val updateOpDeltaObject = Delta( - patches = options.toPatches(publicKeys), - updateCommitment = commitment - ) - - val deltaHash = deltaHash(updateOpDeltaObject) - - val updateOpSignedData = UpdateOperationSignedData( - updateKey = updatePublicKey, - deltaHash = deltaHash, - ) - val signedJwsObject = sign(updateOpSignedData, keyManager, options.updateKeyAlias) - - return Pair( - SidetreeUpdateOperation( - type = "update", - didSuffix = parsedDid.methodSpecificId, - revealValue = reveal, - delta = updateOpDeltaObject, - signedData = signedJwsObject.serialize(false), - ), - KeyAliases( - updateKeyAlias = newUpdateKeyAlias, - verificationKeyAliases = publicKeysWithAliases.mapNotNull { it.first }, - recoveryKeyAlias = null - ) - ) - } - - private fun sign(serializableObject: Any, keyManager: KeyManager, signKeyAlias: String): JWSObject { - val header = JWSHeader.Builder(JWSAlgorithm.ES256K).build() - val payload = Payload(mapper.writeValueAsString(serializableObject)) - val jwsObject = JWSObject(header, payload) - val signatureBytes = keyManager.sign(signKeyAlias, jwsObject.signingInput) - - val base64UrlEncodedSignature = Base64URL(Convert(signatureBytes).toBase64Url(padding = false)) - return JWSObject( - jwsObject.header.toBase64URL(), - jwsObject.payload.toBase64URL(), - base64UrlEncodedSignature, - ) - } - - private fun deltaHash(updateOpDeltaObject: Delta): String { - val canonicalizedOp = canonicalized(updateOpDeltaObject) - val opMultihash = multihash(canonicalizedOp) - - return Convert(opMultihash).toBase64Url(padding = false) - } - - private fun validateDidDocumentKeys(publicKeys: Iterable) { - val publicKeyIdSet = HashSet() - for (publicKey in publicKeys) { - validateId(publicKey.id) - if (publicKeyIdSet.contains(publicKey.id)) { - throw IllegalArgumentException("DID Document key with ID \"${publicKey.id}\" already exists.") - } - publicKeyIdSet.add(publicKey.id) - - validatePublicKeyPurposes(publicKey.purposes) - } - } - - private fun validatePublicKeyPurposes(purposes: Iterable) { - val processedPurposes = HashSet() - for (purpose in purposes) { - if (processedPurposes.contains(purpose)) { - throw IllegalArgumentException("Public key purpose \"${purpose.code}\" already specified.") - } - processedPurposes.add(purpose) - } - } - - internal fun createOperation(keyManager: KeyManager, options: CreateDidIonOptions?) - : Pair { - val updateKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val updatePublicJwk = keyManager.getPublicKey(updateKeyAlias) - - val publicKeyCommitment = updatePublicJwk.commitment() - - val publicKeysWithAlias = publicKeysWithAliasesToAdd(options, keyManager) - val publicKeysToAdd = publicKeysWithAlias.map { it.second } - validateDidDocumentKeys(publicKeysToAdd) - - validateServices(options?.servicesToAdd ?: emptyList()) - - val createOperationDelta = Delta( - patches = options.toPatches(publicKeysToAdd), - updateCommitment = publicKeyCommitment - ) - - val recoveryKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val recoveryPublicJwk = keyManager.getPublicKey(recoveryKeyAlias) - val recoveryCommitment = recoveryPublicJwk.commitment() - - val operation: OperationSuffixDataObject = - createOperationSuffixDataObject(createOperationDelta, recoveryCommitment) - - return Pair( - SidetreeCreateOperation( - type = "create", - suffixData = operation, - delta = createOperationDelta, - ), - KeyAliases( - updateKeyAlias = updateKeyAlias, - verificationKeyAliases = publicKeysWithAlias.mapNotNull { it.first }, - recoveryKeyAlias = recoveryKeyAlias - ) - ) - } - - private fun publicKeysWithAliasesToAdd(options: CommonOptions?, keyManager: KeyManager) = - if (options == null || options.verificationMethodsToAdd.count() == 0) { - listOf( - VerificationMethodCreationParams( - AlgorithmId.secp256k1, - relationships = listOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD) - ) - ).toPublicKeys(keyManager) - } else { - options.verificationMethodsToAdd.toPublicKeys(keyManager) - } - - private fun validateServices(services: Iterable) = services.forEach { - validateService(it) - } - - private fun validateService(service: Service) { - validateId(service.id) - - require(service.type.length < maxServiceTypeLength) { - "service type \"${service.type}\" exceeds max allowed length of $maxServiceTypeLength" - } - - try { - URI.create(service.serviceEndpoint) - } catch (e: Exception) { - throw IllegalArgumentException("service endpoint is not a valid URI", e) - } - } - - private fun validateId(id: String) { - require(isBase64UrlString(id)) { "id \"$id\" is not base 64 url charset" } - - require(id.length <= maxIdLength) { - "id \"$id\" exceeds max allowed length of $maxIdLength" - } - } - - private fun isBase64UrlString(input: String): Boolean { - return base64UrlCharsetRegex.matches(input) - } - - private fun createOperationSuffixDataObject( - createOperationDeltaObject: Delta, - recoveryCommitment: Commitment): OperationSuffixDataObject { - val jsonString = mapper.writeValueAsString(createOperationDeltaObject) - val canonicalized = JsonCanonicalizer(jsonString).encodedUTF8 - val deltaMultihash = multihash(canonicalized) - - return OperationSuffixDataObject( - deltaHash = Convert(deltaMultihash).toBase64Url(padding = false), - recoveryCommitment = recoveryCommitment - ) - } - - internal fun createRecoverOperation(keyManager: KeyManager, did: String, options: RecoverDidIonOptions): - Pair { - val parsedDid = DID.fromString(did) - require(!parsedDid.methodSpecificId.contains(":")) { - "recovering a DID is only allowed for short form dids, but got $did" - } - - val recoveryPublicKey = keyManager.getPublicKey(options.recoveryKeyAlias) - val reveal = recoveryPublicKey.reveal() - - val nextRecoveryKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val nextRecoveryPublicKey = keyManager.getPublicKey(nextRecoveryKeyAlias) - val nextRecoveryCommitment = nextRecoveryPublicKey.commitment() - - val nextUpdateKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val nextUpdatePublicKey = keyManager.getPublicKey(nextUpdateKeyAlias) - val nextUpdateCommitment = nextUpdatePublicKey.commitment() - - val publicKeyWithAliases = publicKeysWithAliasesToAdd(options, keyManager) - val publicKeysToAdd = publicKeyWithAliases.map { it.second } - validateDidDocumentKeys(publicKeysToAdd) - - validateServices(options.servicesToAdd) - - val delta = Delta( - patches = options.toPatches(publicKeysToAdd), - updateCommitment = nextUpdateCommitment - ) - val deltaHash = deltaHash(delta) - - val dataToBeSigned = RecoveryUpdateSignedData( - recoveryCommitment = nextRecoveryCommitment, - recoveryKey = recoveryPublicKey, - deltaHash = deltaHash - ) - - val jwsObject = sign(dataToBeSigned, keyManager, options.recoveryKeyAlias) - - return Pair( - SidetreeRecoverOperation( - type = "recover", - didSuffix = parsedDid.methodSpecificId, - revealValue = reveal, - delta = delta, - signedData = jwsObject.serialize(), - ), - KeyAliases( - updateKeyAlias = nextUpdateKeyAlias, - verificationKeyAliases = publicKeyWithAliases.mapNotNull { it.first }, - recoveryKeyAlias = nextRecoveryKeyAlias, - ) - ) - } - - private fun createDeactivateOperation( - keyManager: KeyManager, - did: String, - options: DeactivateDidIonOptions): SidetreeDeactivateOperation { - val parsedDid = DID.fromString(did) - require(!parsedDid.methodSpecificId.contains(":")) { - "deactivating a DID is only allowed for short form dids, but got $did" - } - val recoveryPublicKey = keyManager.getPublicKey(options.recoveryKeyAlias) - val reveal = recoveryPublicKey.reveal() - - - val dataToBeSigned = DeactivateUpdateSignedData( - didSuffix = parsedDid.methodSpecificId, - recoveryKey = recoveryPublicKey, - ) - - val jwsObject = sign(dataToBeSigned, keyManager, options.recoveryKeyAlias) - - return SidetreeDeactivateOperation( - type = "deactivate", - didSuffix = parsedDid.methodSpecificId, - revealValue = reveal, - signedData = jwsObject.serialize(), - ) - } - - /** - * Recovers [did] with the given [options]. The `recoveryKeyAlias` value must be available in the [keyManager]. - * Depending on the options provided, will create new keys using [keyManager]. See [RecoverDidIonOptions] for more - * details. - */ - public fun recover(keyManager: KeyManager, did: String, options: RecoverDidIonOptions): IonRecoverResult { - val (recoverOp, keyAliases) = createRecoverOperation(keyManager, did, options) - - val response: HttpResponse = runBlocking { - client.post(operationsEndpoint) { - contentType(ContentType.Application.Json) - setBody(recoverOp) - } - } - - val opBody = runBlocking { - response.bodyAsText() - } - return IonRecoverResult( - keyAliases = keyAliases, - recoverOperation = recoverOp, - operationsResponse = opBody, - ) - } - - /** - * Deactivates [did] with the given [options]. The `recoveryKeyAlias` value must be available in the [keyManager]. - */ - public fun deactivate(keyManager: KeyManager, did: String, options: DeactivateDidIonOptions): IonDeactivateResult { - val deactivateOp = createDeactivateOperation(keyManager, did, options) - - val response: HttpResponse = runBlocking { - client.post(operationsEndpoint) { - contentType(ContentType.Application.Json) - setBody(deactivateOp) - } - } - - val opBody = runBlocking { - response.bodyAsText() - } - - return IonDeactivateResult( - deactivateOperation = deactivateOp, - operationsResponse = opBody, - ) - } -} - -private fun CommonOptions?.toPatches(publicKeysToAdd: Iterable): Iterable { - return listOf( - ReplaceAction( - Document( - publicKeys = publicKeysToAdd, - services = this?.servicesToAdd ?: emptyList() - ) - ) - ) -} - -/** - * Data associated with the [DidIonApi.deactivate] call. Useful for debugging and testing purposes. - */ -public class IonDeactivateResult( - public val deactivateOperation: SidetreeDeactivateOperation, - public val operationsResponse: String) - -/** - * All the data associated with the [recover] call. Useful for advanced, and debugging, purposes. - */ -public class IonRecoverResult( - public val keyAliases: KeyAliases, - public val recoverOperation: SidetreeRecoverOperation, - public val operationsResponse: String) - -private interface CommonOptions { - val verificationMethodsToAdd: Iterable - val servicesToAdd: Iterable -} - -private fun JWK.commitment(): Commitment { - require(!this.isPrivate) { throw IllegalArgumentException("provided JWK must not be a private key") } - - val pkJson = this.toJSONString() - val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 - - val sha256 = MessageDigest.getInstance("SHA-256") - val pkDigest = sha256.digest(canonicalized) - - val pkDigestMultihash = multihash(pkDigest) - return Commitment(pkDigestMultihash) -} - -private fun JWK.reveal(): Reveal { - require(!this.isPrivate) { throw IllegalArgumentException("provided JWK must not be a private key") } - - val pkJson = this.toJSONString() - val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 - - val mh = multihash(canonicalized) - return Reveal(mh) -} - -/** - * Computes a multihash of the given payload. - * - * A multihash is a protocol for differentiating outputs from various well-established cryptographic hash functions, - * addressing size and encoding considerations. - * - * This function specifically calculates the SHA-256 hash of the input payload, then prefixes the result with - * the multicodec identifier for SHA-256 and the digest length. The multicodec identifier is a predetermined - * byte array - * - * @param payload The input data for which the multihash needs to be calculated. - * @return A byte array representing the multihash of the input payload. It includes the multicodec prefix, - * the length of the hash digest, and the hash digest itself. - */ -public fun multihash(payload: ByteArray): ByteArray { - val sha256 = MessageDigest.getInstance("SHA-256") - sha256.update(payload) - - val digestLen = sha256.digestLength - val digest = sha256.digest() - - return sha256MultiCodec + Varint.encode(digestLen) + digest -} - -/** - * Metadata related to the update of an ion DID. - */ -public data class IonUpdateResult( - public val operationsResponseBody: String, - public val keyAliases: KeyAliases -) - -/** - * Represents an HTTP response where the status code is outside the range considered success. - */ -public class InvalidStatusException(public val statusCode: Int, msg: String) : RuntimeException(msg) - -/** - * Represents an exception where the response from calling [DidIonApi.resolve] contains a non-empty value in - * [DidResolutionMetadata.error]. - * - * Note: This exception is only thrown when calling [DidIonApi.create]. Callers of [DidIonApi.resolve] should - * handle possible values of [DidResolutionMetadata.error] within [DidResolutionResult]. - */ -public class ResolutionException(msg: String) : RuntimeException(msg) - -/** - * Container for the key aliases for an ION did. - */ -public data class KeyAliases( - public val updateKeyAlias: String?, - public val verificationKeyAliases: List, - public val recoveryKeyAlias: String?) - -/** - * Options available when creating an ion did. - * - * - * @param verificationMethodsToAdd List of specs that will be added to the DID ION document. - * @param servicesToAdd When provided, the services will be added to the DID document. Note that for each of the - * services that should be added, the following must hold: - * - The `id` field cannot be over 50 chars and must only use characters from the Base64URL character set. - * - The `type` field cannot be over 30 characters. - * - The `serviceEndpoint` must be a valid URI. - */ -public class CreateDidIonOptions( - override val verificationMethodsToAdd: Iterable = emptyList(), - override val servicesToAdd: Iterable = emptyList(), -) : CreateDidOptions, CommonOptions - -/** Common interface for options available when adding a VerificationMethod. */ -public interface VerificationMethodSpec - -private interface VerificationMethodGenerator { - fun generate(): Pair -} - -/** - * A [VerificationMethodSpec] where a [KeyManager] will be used to generate the underlying verification method keys. - * The parameters [algorithmId] and [options] will be forwarded to the keyManager. - * - * [relationships] will be used to determine the verification relationships in the DID Document being created. - * */ -public class VerificationMethodCreationParams( - public val algorithmId: AlgorithmId, - public val options: KeyGenOptions? = null, - public val relationships: Iterable -) : VerificationMethodSpec { - internal fun toGenerator(keyManager: KeyManager): VerificationMethodKeyManagerGenerator { - return VerificationMethodKeyManagerGenerator(keyManager, this) - } -} - -/** - * A [VerificationMethodSpec] according to https://w3c-ccg.github.io/lds-jws2020/. - * - * The [id] property cannot be over 50 chars and must only use characters from the Base64URL character set. - */ -public class JsonWebKey2020VerificationMethod( - public val id: String, - public val controller: String? = null, - public val publicKeyJwk: JWK, - public val relationships: Iterable = emptySet() -) : VerificationMethodSpec, VerificationMethodGenerator { - override fun generate(): Pair { - return Pair(null, PublicKey(id, "JsonWebKey2020", controller, publicKeyJwk, relationships)) - } -} - -/** - * A [VerificationMethodSpec] according to https://w3c-ccg.github.io/lds-ecdsa-secp256k1-2019/. - * - * The [id] property cannot be over 50 chars and must only use characters from the Base64URL character set. - */ -public class EcdsaSecp256k1VerificationKey2019VerificationMethod( - public val id: String, - public val controller: String? = null, - public val publicKeyJwk: JWK, - public val relationships: Iterable = emptySet() -) : VerificationMethodSpec, VerificationMethodGenerator { - override fun generate(): Pair { - return Pair(id, PublicKey(id, "EcdsaSecp256k1VerificationKey2019", controller, publicKeyJwk, relationships)) - } -} - -internal class VerificationMethodKeyManagerGenerator( - val keyManager: KeyManager, - val params: VerificationMethodCreationParams, -) : VerificationMethodGenerator { - - override fun generate(): Pair { - val alias = keyManager.generatePrivateKey( - algorithmId = params.algorithmId, - options = params.options - ) - val publicKeyJwk = keyManager.getPublicKey(alias) - return Pair( - alias, - PublicKey( - id = UUID.randomUUID().toString(), - type = "JsonWebKey2020", - publicKeyJwk = publicKeyJwk, - purposes = params.relationships, - ) - ) - } -} - - -private fun Iterable.toGenerators(keyManager: KeyManager): List { - return buildList { - for (verificationMethodSpec in this@toGenerators) { - when (verificationMethodSpec) { - is VerificationMethodCreationParams -> add(verificationMethodSpec.toGenerator(keyManager)) - - is VerificationMethodGenerator -> add(verificationMethodSpec) - } - } - } - -} - -private fun Iterable.toPublicKeys(keyManager: KeyManager) = toGenerators( - keyManager -).map { it.generate() } - -/** - * Metadata related to the creation of a DID (Decentralized Identifier) on the Sidetree protocol. - * - * @property createOperation The Sidetree create operation used to create the DID. - * @property shortFormDid The short-form DID representing the DID created. - * @property longFormDid The long-form DID representing the DID created. - * @property operationsResponseBody The response body received after submitting the create operation. - */ -public data class IonCreationMetadata( - public val createOperation: SidetreeCreateOperation, - public val shortFormDid: String, - public val longFormDid: String, - public val operationsResponseBody: String, - public val keyAliases: KeyAliases, -) : CreationMetadata \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/ion/models/Models.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/ion/models/Models.kt deleted file mode 100644 index d0d141a19..000000000 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/ion/models/Models.kt +++ /dev/null @@ -1,307 +0,0 @@ -package web5.sdk.dids.methods.ion.models - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonValue -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import com.nimbusds.jose.jwk.JWK -import web5.sdk.common.Convert -import web5.sdk.common.EncodingFormat -import web5.sdk.dids.PublicKeyPurpose - -/** - * Represents an ION document containing public keys and services. See bullet 2 in https://identity.foundation/sidetree/spec/#replace. - * - * @property publicKeys Iterable of public keys. - * @property services Iterable of services. - */ -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public data class Document( - val publicKeys: Iterable = emptyList(), - val services: Iterable = emptyList() -) - -/** - * Represents an ION service. See bullet 3 in https://identity.foundation/sidetree/spec/#add-services. - * - * @property id The service ID. - * @property type The service type. - * @property serviceEndpoint The service endpoint. - */ -public data class Service( - public val id: String, - public val type: String, - public val serviceEndpoint: String -) - -/** - * Represents a public key in the ION document as defined in item 3 of https://identity.foundation/sidetree/spec/#add-public-keys - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public data class PublicKey( - public val id: String, - public val type: String, - public val controller: String? = null, - - @JsonSerialize(using = JacksonJwk.Serializer::class) - @JsonDeserialize(using = JacksonJwk.Deserializer::class) - public val publicKeyJwk: JWK, - public val purposes: Iterable = emptyList() -) - -/** - * JacksonJWK is a utility class that facilitates serialization for [JWK] types, so that it's easy to integrate with any - * class that is meant to be serialized to/from JSON. - */ -private class JacksonJwk { - /** - * [Serializer] implements [JsonSerializer] for use with the [JsonSerialize] annotation from Jackson. - */ - object Serializer : JsonSerializer() { - override fun serialize(value: JWK, gen: JsonGenerator, serializers: SerializerProvider) { - with(gen) { - writeObject(value.toJSONObject()) - } - } - } - - /** - * [Deserializer] implements [JsonDeserializer] for use with the [JsonDeserialize] annotation from Jackson. - */ - object Deserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): JWK { - val typeRef = object : TypeReference>() {} - val node = p.readValueAs(typeRef) as HashMap - return JWK.parse(node) - } - } -} - -/** - * Sealed class representing a patch action in the ION document. See https://identity.foundation/sidetree/spec/#did-state-patches - */ -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "action" -) -@JsonSubTypes( - JsonSubTypes.Type(AddServicesAction::class, name = "add-services"), - JsonSubTypes.Type(ReplaceAction::class, name = "replace"), - JsonSubTypes.Type(RemoveServicesAction::class, name = "remove-services"), - JsonSubTypes.Type(AddPublicKeysAction::class, name = "add-public-keys"), - JsonSubTypes.Type(RemovePublicKeysAction::class, name = "remove-public-keys"), -) -public interface PatchAction - -/** - * Represents an "add_services" patch action in the ION document as defined in https://identity.foundation/sidetree/spec/#add-services. - * - * @property services Iterable of services to add. - */ -public data class AddServicesAction( - public val services: Iterable = emptyList() -) : PatchAction - -/** - * Represents a "replace" patch action in the ION document as defined in https://identity.foundation/sidetree/spec/#replace. - * - * @property document The document to replace. - */ -public data class ReplaceAction( - val document: Document? = null -) : PatchAction - -/** Model for https://identity.foundation/sidetree/spec/#remove-services */ -public data class RemoveServicesAction( - val ids: Iterable -) : PatchAction - -/** Model for https://identity.foundation/sidetree/spec/#add-public-keys */ -public data class AddPublicKeysAction( - val publicKeys: Iterable -) : PatchAction - -/** Model for https://identity.foundation/sidetree/spec/#remove-public-keys */ -public data class RemovePublicKeysAction( - val ids: Iterable -) : PatchAction - -/** - * Represents a delta in the ION document as defined in bullet 3 of https://identity.foundation/sidetree/spec/#create - * - * @property patches Iterable of patch actions. - * @property updateCommitment Update commitment. - */ -public data class Delta( - public val patches: Iterable, - public val updateCommitment: Commitment -) - -/** - * Represents operation suffix data object as defined in bullet 6 of https://identity.foundation/sidetree/spec/#create - * - * @property deltaHash Delta hash. - * @property recoveryCommitment Recovery commitment. - */ -public data class OperationSuffixDataObject( - public val deltaHash: String, - public val recoveryCommitment: Commitment -) - -/** - * Represents the commitment value as defined in item 3 of https://identity.foundation/sidetree/spec/#public-key-commitment-scheme. - */ -@JsonSerialize(using = CommitmentSerializer::class) -@JsonDeserialize(using = CommitmentDeserializer::class) -public class Commitment(public override val bytes: ByteArray) : BytesField - -private class CommitmentSerializer : StdSerializer(Commitment::class.java) { - override fun serialize(value: Commitment?, gen: JsonGenerator, provider: SerializerProvider?) { - with(gen) { - writeString(value?.toBase64Url()) - } - } -} - -private class CommitmentDeserializer : FromStringDeserializer(Commitment::class.java) { - override fun _deserialize(value: String?, ctxt: DeserializationContext?): Commitment { - return Commitment(Convert(value, EncodingFormat.Base64Url).toByteArray()) - } -} - -/** - * Represents the reveal value as defined in item 3 of https://identity.foundation/sidetree/spec/#public-key-commitment-scheme. - */ -@JsonSerialize(using = RevealSerializer::class) -@JsonDeserialize(using = RevealDeserializer::class) -public class Reveal(public override val bytes: ByteArray) : BytesField - -private class RevealSerializer : StdSerializer(Reveal::class.java) { - override fun serialize(value: Reveal?, gen: JsonGenerator, provider: SerializerProvider?) { - with(gen) { - writeString(value?.toBase64Url()) - } - } -} - -internal interface BytesField { - val bytes: ByteArray - - fun toBase64Url(): String { - return Convert(bytes).toBase64Url(padding = false) - } -} - -private class RevealDeserializer : FromStringDeserializer( - Reveal::class.java -) { - override fun _deserialize(value: String?, ctxt: DeserializationContext?): Reveal { - return Reveal(Convert(value, EncodingFormat.Base64Url).toByteArray()) - } -} - - -/** - * Sidetree API create operation as defined in https://identity.foundation/sidetree/api/#create - */ -public data class SidetreeCreateOperation( - public val type: String, - public val delta: Delta, - public val suffixData: OperationSuffixDataObject) { - -} - -/** - * Sidetree update operation as defined in https://identity.foundation/sidetree/api/#update - */ -public data class SidetreeUpdateOperation( - public val type: String, - public val didSuffix: String, - public val revealValue: Reveal, - public val delta: Delta, - public val signedData: String, -) - -/** - * Sidetree recover operation as defined in https://identity.foundation/sidetree/api/#recover - */ -public data class SidetreeRecoverOperation( - public val type: String, - public val didSuffix: String, - public val revealValue: Reveal, - public val delta: Delta, - public val signedData: String, -) - -/** - * Update operation signed data object as defined in https://identity.foundation/sidetree/spec/#update-signed-data-object - */ -public data class UpdateOperationSignedData( - @JsonSerialize(using = JacksonJwk.Serializer::class) - @JsonDeserialize(using = JacksonJwk.Deserializer::class) - public val updateKey: JWK, - public val deltaHash: String, -) - -/** - * InitialState is the initial state of a DID Document as defined in the spec - * https://identity.foundation/sidetree/spec/#long-form-did-uris - */ -internal data class InitialState( - val suffixData: OperationSuffixDataObject, - val delta: Delta, -) - -/** - * Metadata about the did method as defined in bullet 3 (subitem 'method') of https://identity.foundation/sidetree/spec/#did-resolver-output - */ -public class MetadataMethod( - public val published: Boolean, - public val recoveryCommitment: Commitment, - public val updateCommitment: Commitment, -) - -/** - * Recovery operation signed data object as defined in https://identity.foundation/sidetree/spec/#recovery-signed-data-object - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class RecoveryUpdateSignedData( - public val recoveryCommitment: Commitment, - - @JsonSerialize(using = JacksonJwk.Serializer::class) - @JsonDeserialize(using = JacksonJwk.Deserializer::class) - public val recoveryKey: JWK, - public val deltaHash: String, - public val anchorOrigin: String? = null) - - -/** - * Deactivate operation signed data object as defined in https://identity.foundation/sidetree/spec/#deactivate-signed-data-object - */ -public class DeactivateUpdateSignedData( - public val didSuffix: String, - - @JsonSerialize(using = JacksonJwk.Serializer::class) - @JsonDeserialize(using = JacksonJwk.Deserializer::class) - public val recoveryKey: JWK) - -/** - * Sidetree recover operation as defined in https://identity.foundation/sidetree/api/#deactivate - */ -public class SidetreeDeactivateOperation( - public val type: String, - public val didSuffix: String, - public val revealValue: Reveal, - public val signedData: String) \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 0a6212397..ecdd614b9 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -2,10 +2,6 @@ package web5.sdk.dids.methods.jwk import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.KeyUse -import foundation.identity.did.DID -import foundation.identity.did.DIDDocument -import foundation.identity.did.VerificationMethod -import foundation.identity.did.parser.ParserException import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.crypto.AlgorithmId @@ -17,8 +13,12 @@ import web5.sdk.dids.DidResolutionMetadata import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.didcore.VerificationMethod +import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.validateKeyMaterialInsideKeyManager -import java.net.URI import java.text.ParseException /** @@ -26,7 +26,7 @@ import java.text.ParseException * * @property algorithmId Specifies the algorithmId to be used for key creation. * Defaults to ES256K (Elliptic Curve Digital Signature Algorithm with SHA-256 and secp256k1 curve). - * @constructor Creates an instance of [CreateDidJwkOptions] with the provided [algorithm] and [curve]. + * @constructor Creates an instance of [CreateDidJwkOptions] with the provided [algorithmId] * * ### Usage Example: * ``` @@ -107,8 +107,8 @@ public class DidJwk(uri: String, keyManager: KeyManager) : Did(uri, keyManager) * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. */ override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { - val parsedDid = try { - DID.fromString(did) + val parsedDidUri = try { + DidUri.parse(did) } catch (_: ParserException) { return DidResolutionResult( context = "https://w3id.org/did-resolution/v1", @@ -118,7 +118,7 @@ public class DidJwk(uri: String, keyManager: KeyManager) : Did(uri, keyManager) ) } - if (parsedDid.methodName != methodName) { + if (parsedDidUri.method != methodName) { return DidResolutionResult( context = "https://w3id.org/did-resolution/v1", didResolutionMetadata = DidResolutionMetadata( @@ -127,7 +127,7 @@ public class DidJwk(uri: String, keyManager: KeyManager) : Did(uri, keyManager) ) } - val id = parsedDid.methodSpecificId + val id = parsedDidUri.id val decodedKey = Convert(id, EncodingFormat.Base64Url).toStr() val publicKeyJwk = try { JWK.parse(decodedKey) @@ -144,36 +144,33 @@ public class DidJwk(uri: String, keyManager: KeyManager) : Did(uri, keyManager) throw IllegalArgumentException("decoded jwk value cannot be a private key") } - val verificationMethodId = URI.create("$did#0") - val verificationMethod = VerificationMethod.builder() + val verificationMethodId = "${parsedDidUri.uri}#0" + val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) - .publicKeyJwk(publicKeyJwk.toJSONObject()) - .controller(URI(did)) - .type("JsonWebKey2020") + .publicKeyJwk(publicKeyJwk) + .controller(did) + .type("JsonWebKey") .build() - val verificationMethodRef = VerificationMethod.builder() - .id(verificationMethodId) - .build() - - val didDocumentBuilder = DIDDocument.builder() - .contexts( - mutableListOf( - URI.create("https://w3id.org/security/suites/jws-2020/v1") - ) - ) - .id(URI(did)) - .verificationMethod(verificationMethod) + val didDocumentBuilder = DIDDocument.Builder() + .context(listOf("https://www.w3.org/ns/did/v1")) + .id(did) if (publicKeyJwk.keyUse != KeyUse.ENCRYPTION) { didDocumentBuilder - .assertionMethodVerificationMethod(verificationMethodRef) - .authenticationVerificationMethod(verificationMethodRef) - .capabilityDelegationVerificationMethods(listOf(verificationMethodRef)) - .capabilityInvocationVerificationMethod(verificationMethodRef) + .verificationMethodForPurposes( + verificationMethod, + listOf( + Purpose.AssertionMethod, + Purpose.Authentication, + Purpose.CapabilityDelegation, + Purpose.CapabilityInvocation + ) + ) } + if (publicKeyJwk.keyUse != KeyUse.SIGNATURE) { - didDocumentBuilder.keyAgreementVerificationMethod(verificationMethodRef) + didDocumentBuilder.verificationMethodForPurposes(verificationMethod, listOf(Purpose.KeyAgreement)) } val didDocument = didDocumentBuilder.build() diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 1cd3d0782..c2e78532e 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -1,8 +1,5 @@ package web5.sdk.dids.methods.key -import foundation.identity.did.DID -import foundation.identity.did.DIDDocument -import foundation.identity.did.VerificationMethod import io.ipfs.multibase.Multibase import web5.sdk.common.Varint import web5.sdk.crypto.AlgorithmId @@ -14,8 +11,11 @@ import web5.sdk.dids.Did import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.didcore.VerificationMethod import web5.sdk.dids.validateKeyMaterialInsideKeyManager -import java.net.URI /** * Specifies options for creating a new "did:key" Decentralized Identifier (DID). @@ -127,11 +127,11 @@ public class DidKey(uri: String, keyManager: KeyManager) : Did(uri, keyManager) * @throws IllegalArgumentException if the provided DID does not conform to the "did:key" method. */ override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { - val parsedDid = DID.fromString(did) + val parsedDidUri = DidUri.parse(did) - require(parsedDid.methodName == methodName) { throw IllegalArgumentException("expected did:key") } + require(parsedDidUri.method == methodName) { throw IllegalArgumentException("expected did:key") } - val id = parsedDid.methodSpecificId + val id = parsedDidUri.id val idBytes = Multibase.decode(id) val (multiCodec, numBytes) = Varint.decode(idBytes) @@ -144,26 +144,25 @@ public class DidKey(uri: String, keyManager: KeyManager) : Did(uri, keyManager) val publicKeyJwk = keyGenerator.bytesToPublicKey(publicKeyBytes) - val verificationMethodId = URI.create("$did#$id") - val verificationMethod = VerificationMethod.builder() + val verificationMethodId = "${parsedDidUri.uri}#$id" + val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) - .publicKeyJwk(publicKeyJwk.toJSONObject()) - .controller(URI(did)) + .publicKeyJwk(publicKeyJwk) + .controller(did) .type("JsonWebKey2020") .build() - val verificationMethodRef = VerificationMethod.builder() - .id(verificationMethodId) - .build() - - val didDocument = DIDDocument.builder() - .id(URI(did)) - .verificationMethod(verificationMethod) - .assertionMethodVerificationMethod(verificationMethodRef) - .authenticationVerificationMethod(verificationMethodRef) - .capabilityDelegationVerificationMethods(listOf(verificationMethodRef)) - .capabilityInvocationVerificationMethod(verificationMethodRef) - .keyAgreementVerificationMethod(verificationMethodRef) + val didDocument = DIDDocument.Builder() + .id(did) + .verificationMethodForPurposes( + verificationMethod, + listOf( + Purpose.AssertionMethod, + Purpose.Authentication, + Purpose.KeyAgreement, + Purpose.CapabilityDelegation, + Purpose.CapabilityInvocation + )) .build() return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 69a1fad43..8d6521078 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -1,19 +1,17 @@ package web5.sdk.dids.methods.web import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import foundation.identity.did.DID -import foundation.identity.did.DIDDocument -import foundation.identity.did.parser.ParserException import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.ResponseException import io.ktor.client.request.get -import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText -import io.ktor.http.ContentType +import io.ktor.client.statement.HttpResponse import io.ktor.http.contentType +import io.ktor.http.ContentType import io.ktor.http.isSuccess import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking @@ -23,12 +21,14 @@ import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps import web5.sdk.crypto.KeyManager import web5.sdk.dids.CreateDidOptions +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.Did import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult +import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.methods.ion.InvalidStatusException import web5.sdk.dids.validateKeyMaterialInsideKeyManager import java.io.File import java.net.InetAddress @@ -87,8 +87,8 @@ public fun DidWebApi(blockConfiguration: DidWebApiConfiguration.() -> Unit): Did private class DidWebApiImpl(configuration: DidWebApiConfiguration) : DidWebApi(configuration) -private const val wellKnownURLPath = "/.well-known" -private const val didDocFilename = "/did.json" +private const val WELL_KNOWN_URL_PATH = "/.well-known" +private const val DID_DOC_FILE_NAME = "/did.json" /** * Implements [resolve] and [create] according to https://w3c-ccg.github.io/did-method-web/ @@ -132,16 +132,16 @@ public sealed class DidWebApi( } private fun resolveInternal(did: String, options: ResolveDidOptions?): DidResolutionResult { - val parsedDid = try { - DID.fromString(did) + val parsedDidUri = try { + DidUri.parse(did) } catch (_: ParserException) { return DidResolutionResult.fromResolutionError(ResolutionError.INVALID_DID) } - if (parsedDid.methodName != methodName) { + if (parsedDidUri.method != methodName) { return DidResolutionResult.fromResolutionError(ResolutionError.METHOD_NOT_SUPPORTED) } - val docURL = getDocURL(parsedDid) + val docURL = getDocURL(parsedDidUri) val resp: HttpResponse = try { runBlocking { @@ -156,7 +156,7 @@ public sealed class DidWebApi( val body = runBlocking { resp.bodyAsText() } if (!resp.status.isSuccess()) { - throw InvalidStatusException(resp.status.value, "resolution error response: '$body'") + throw ResponseException(resp, "resolution error response: '$body'") } return DidResolutionResult( didDocument = mapper.readValue(body, DIDDocument::class.java), @@ -168,17 +168,17 @@ public sealed class DidWebApi( return DidWeb(uri, keyManager, this) } - private fun getDocURL(parsedDid: DID): String { - val domainNameWithPath = parsedDid.methodSpecificId.replace(":", "/") + private fun getDocURL(parsedDidUri: DidUri): String { + val domainNameWithPath = parsedDidUri.id.replace(":", "/") val decodedDomain = URLDecoder.decode(domainNameWithPath, UTF_8) val targetUrl = StringBuilder("https://$decodedDomain") val url = URL(targetUrl.toString()) if (url.path.isEmpty()) { - targetUrl.append(wellKnownURLPath) + targetUrl.append(WELL_KNOWN_URL_PATH) } - targetUrl.append(didDocFilename) + targetUrl.append(DID_DOC_FILE_NAME) return targetUrl.toString() } diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt index 64b52e0fe..27b31b2ff 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt @@ -1,6 +1,5 @@ package web5.sdk.dids -import foundation.identity.did.DID import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpHeaders @@ -10,9 +9,11 @@ import io.ktor.utils.io.ByteReadChannel import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.didcore.DidUri import web5.sdk.dids.methods.key.DidKey import web5.sdk.dids.methods.web.DidWebApi import java.security.SignatureException +import kotlin.test.assertContains import kotlin.test.assertEquals class DidMethodTest { @@ -22,7 +23,7 @@ class DidMethodTest { val did = DidKey.create(manager) val verificationMethod = did.resolve().didDocument!!.findAssertionMethodById() - assertEquals("${did.uri}#${DID.fromString(did.uri).methodSpecificId}", verificationMethod.id.toString()) + assertEquals("${did.uri}#${DidUri.parse(did.uri).id}", verificationMethod.id) } @Test @@ -30,9 +31,9 @@ class DidMethodTest { val manager = InMemoryKeyManager() val did = DidKey.create(manager) - val assertionMethodId = "${did.uri}#${DID.fromString(did.uri).methodSpecificId}" + val assertionMethodId = "${did.uri}#${DidUri.parse(did.uri).id}" val verificationMethod = did.resolve().didDocument!!.findAssertionMethodById(assertionMethodId) - assertEquals(assertionMethodId, verificationMethod.id.toString()) + assertEquals(assertionMethodId, verificationMethod.id) } @Test @@ -43,7 +44,7 @@ class DidMethodTest { val exception = assertThrows { did.resolve().didDocument!!.findAssertionMethodById("made up assertion method id") } - assertEquals("assertion method \"made up assertion method id\" not found", exception.message) + assertContains(exception.message!!, "assertion method \"made up assertion method id\" not found") } @Test diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt index 56c2c7b8c..d83c376bf 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt @@ -2,8 +2,10 @@ package web5.sdk.dids import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.methods.ion.DidIon +import web5.sdk.dids.methods.dht.DidDht +import kotlin.test.assertEquals class DidResolversTest { @@ -14,10 +16,25 @@ class DidResolversTest { } @Test - fun `resolving a default ion did contains assertion method`() { - val ionDid = DidIon.create(InMemoryKeyManager()) + fun `resolving a default dht did contains assertion method`() { + val dhtDid = DidDht.create(InMemoryKeyManager()) - val resolutionResult = DidResolvers.resolve(ionDid.uri) - assertNotNull(resolutionResult.didDocument!!.assertionMethodVerificationMethodsDereferenced) + val resolutionResult = DidResolvers.resolve(dhtDid.uri) + assertNotNull(resolutionResult.didDocument!!.assertionMethod) } -} \ No newline at end of file + + @Test + fun `resolving an invalid did throws an exception`() { + val exception = assertThrows { + DidResolvers.resolve("did:invalid:123") + } + assertEquals("Resolving did:invalid not supported", exception.message) + } + + @Test + fun `addResolver adds a custom resolver`() { + val resolver: DidResolver = { _, _ -> DidResolutionResult(null, null) } + DidResolvers.addResolver("test", resolver) + assertNotNull(DidResolvers.resolve("did:test:123")) + } +} diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt new file mode 100644 index 000000000..0d1dd0bb1 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt @@ -0,0 +1,307 @@ +package web5.sdk.dids.didcore + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import web5.sdk.crypto.AlgorithmId +import web5.sdk.crypto.InMemoryKeyManager +import java.security.SignatureException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DIDDocumentTest { + + @Nested + inner class SelectVerificationMethodTest { + + @Test + fun `selectVerificationMethod throws exception if vmMethod is empty`() { + + val doc = DIDDocument("did:example:123") + + assertThrows { + doc.selectVerificationMethod(Purpose.AssertionMethod) + } + } + + @Test + fun `selectVerificationMethod returns first vm`() { + + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("id", "type", "controller", publicKeyJwk) + ) + val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList) + + val vm = doc.selectVerificationMethod(null) + assertEquals("id", vm.id) + assertEquals("type", vm.type) + assertEquals("controller", vm.controller) + + } + + @Test + fun `selectVerificationMethod returns vm from the purpose specific method`() { + + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("id", "type", "controller", publicKeyJwk) + ) + val assertionMethods = listOf("id") + val doc = DIDDocument( + id = "did:example:123", + verificationMethod = vmList, + assertionMethod = assertionMethods + ) + + val vm = doc.selectVerificationMethod(Purpose.AssertionMethod) + assertEquals("id", vm.id) + assertEquals("type", vm.type) + assertEquals("controller", vm.controller) + + } + + @Test + fun `selectVerificationMethod returns vm from the provided id`() { + + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("id", "type", "controller", publicKeyJwk) + ) + val assertionMethods = listOf("id") + val doc = DIDDocument( + id = "did:example:123", + verificationMethod = vmList, + assertionMethod = assertionMethods + ) + + val vm = doc.selectVerificationMethod(ID("id")) + assertEquals("id", vm.id) + assertEquals("type", vm.type) + assertEquals("controller", vm.controller) + + } + + @Test + fun `selectVerificationMethod throws exception if id cannot be found`() { + + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("id", "type", "controller", publicKeyJwk) + ) + val doc = DIDDocument( + id = "did:example:123", + verificationMethod = vmList + ) + + assertThrows { + doc.selectVerificationMethod(Purpose.AssertionMethod) + } + } + + } + + @Nested + inner class GetAbsoluteResourceIDTest { + @Test + fun `getAbsoluteResourceID returns absolute resource id if passed in fragment`() { + val doc = DIDDocument("did:example:123") + val resourceID = doc.getAbsoluteResourceID("#0") + assertEquals("did:example:123#0", resourceID) + } + + @Test + fun `getAbsoluteResourceID returns absolute resource id if passed in full id`() { + val doc = DIDDocument("did:example:123") + val resourceID = doc.getAbsoluteResourceID("did:example:123#1") + assertEquals("did:example:123#1", resourceID) + } + } + + + @Nested + inner class FindAssertionMethodByIdTest { + @Test + fun `findAssertionMethodById throws exception if assertionMethod list is empty`() { + val doc = DIDDocument("did:example:123") + + assertThrows { + doc.findAssertionMethodById() + } + } + + @Test + fun `findAssertionMethodById throws exception if assertionMethod does not have provided id`() { + val assertionMethods = listOf("foo") + + val doc = DIDDocument(id = "did:example:123", assertionMethod = assertionMethods) + + assertThrows { + doc.findAssertionMethodById("bar") + } + } + + @Test + fun `findAssertionMethodById throws exception if id not found in verificationMethod`() { + val assertionMethods = listOf("bar") + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("foo", "type", "controller", publicKeyJwk) + ) + + val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) + + assertThrows { + doc.findAssertionMethodById() + } + } + + @Test + fun `findAssertionMethodById returns assertion verification method if id is found`() { + val assertionMethods = listOf("foo") + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + + val vmList = listOf( + VerificationMethod("foo", "type", "controller", publicKeyJwk) + ) + + val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) + + val assertionMethod = doc.findAssertionMethodById("foo") + assertEquals("foo", assertionMethod.id) + assertEquals("type", assertionMethod.type) + assertEquals("controller", assertionMethod.controller) + } + } + + @Nested + inner class BuilderTest { + @Test + fun `builder creates a DIDDocument with the provided id`() { + + val svc = Service.Builder() + .id("service_id") + .type("service_type") + .serviceEndpoint(listOf("https://example.com")) + .build() + + val doc = DIDDocument.Builder() + .id("did:ex:foo") + .context(listOf("https://www.w3.org/ns/did/v1")) + .controllers(listOf("did:ex:foo")) + .alsoKnownAses(listOf("did:ex:bar")) + .services(listOf(svc)) + .build() + + assertEquals("did:ex:foo", doc.id) + assertEquals("https://www.w3.org/ns/did/v1", doc.context!!.first()) + assertEquals(listOf("did:ex:foo"), doc.controller) + assertEquals(listOf("did:ex:bar"), doc.alsoKnownAs) + assertEquals(listOf(svc), doc.service) + } + + @Test + fun `verificationMethodForPurposes builds lists`() { + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) + + val doc = DIDDocument.Builder() + .id("did:ex:foo") + .context(listOf("https://www.w3.org/ns/did/v1")) + .verificationMethodForPurposes(vm, + listOf( + Purpose.AssertionMethod, + Purpose.Authentication, + Purpose.KeyAgreement, + Purpose.CapabilityDelegation, + Purpose.CapabilityInvocation) + ) + .build() + + assertEquals(1, doc.verificationMethod?.size) + assertEquals(1, doc.assertionMethod?.size) + assertEquals(1, doc.authentication?.size) + assertEquals(1, doc.keyAgreement?.size) + assertEquals(1, doc.capabilityDelegation?.size) + assertEquals(1, doc.capabilityInvocation?.size) + + } + + @Test + fun `verificationMethodsForPurpose builds list for one purpose`() { + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) + + val doc = DIDDocument.Builder() + .id("did:ex:foo") + .context(listOf("https://www.w3.org/ns/did/v1")) + .verificationMethodForPurposes(vm,listOf(Purpose.Authentication)) + .build() + + assertEquals(1, doc.verificationMethod?.size) + assertEquals(1, doc.authentication?.size) + assertNull(doc.keyAgreement) + assertNull(doc.capabilityInvocation) + assertNull(doc.capabilityDelegation) + assertNull(doc.assertionMethod) + } + + @Test + fun `verificationMethodsForPurpose builds list when no purpose is passed in`() { + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + val publicKeyJwk = manager.getPublicKey(keyAlias) + val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) + + val doc = DIDDocument.Builder() + .id("did:ex:foo") + .context(listOf("https://www.w3.org/ns/did/v1")) + .verificationMethodForPurposes(vm) + .build() + + assertEquals(1, doc.verificationMethod?.size) + assertNull(doc.authentication) + assertNull(doc.keyAgreement) + assertNull(doc.capabilityInvocation) + assertNull(doc.capabilityDelegation) + assertNull(doc.assertionMethod) + } + + @Test + fun `verificationMethodIdsForPurpose builds list for one purpose`() { + val doc = DIDDocument.Builder() + .id("did:ex:foo") + .context(listOf("https://www.w3.org/ns/did/v1")) + .verificationMethodIdsForPurpose(mutableListOf("keyagreementId"), Purpose.KeyAgreement) + .build() + + assertEquals(1, doc.keyAgreement?.size) + assertNull(doc.verificationMethod) + assertNull(doc.authentication) + assertNull(doc.capabilityInvocation) + assertNull(doc.capabilityDelegation) + assertNull(doc.assertionMethod) + } + } +} diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidUriTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidUriTest.kt new file mode 100644 index 000000000..5cb2ee7c1 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidUriTest.kt @@ -0,0 +1,59 @@ +package web5.sdk.dids.didcore + +import org.junit.jupiter.api.assertThrows +import web5.sdk.dids.exceptions.ParserException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull + +class DidUriTest { + + @Test + fun `toString() returns url`() { + val didUri = DidUri( + uri = "did:example:123", + url = "did:example:123#0", + method = "example", + id = "123", + ) + assertEquals("did:example:123#0", didUri.toString()) + } + + @Test + fun `Parser throws exception with invalid did`() { + val invalidDids = listOf( + "", + "did:", + "did:uport", + "did:uport:", + "did:uport:1234_12313***", + "2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX", + "did:method:%12%1", + "did:method:%1233%Ay", + "did:CAP:id", + "did:method:id::anotherid%r9") + for (did in invalidDids) { + val exception = assertThrows { + DidUri.Parser.parse(did) + } + assertEquals("Invalid DID URI", exception.message) + } + } + + @Test + fun `Parser parses a valid did`() { + // todo adding /path after abcdefghi messes up the parsing of params (comes in null) + // to be addressed via gh issue https://github.com/TBD54566975/web5-spec/issues/120 + val didUri = DidUri.Parser.parse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1") + assertEquals("did:example:123456789abcdefghi", didUri.uri) + assertEquals("123456789abcdefghi", didUri.id) + assertEquals("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", didUri.url) + assertEquals("example", didUri.method) + assertEquals("123456789abcdefghi", didUri.id) + assertEquals("foo=bar&baz=qux", didUri.query) + assertEquals("keys-1", didUri.fragment) + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), didUri.params) + } + +} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/ServiceTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/ServiceTest.kt new file mode 100644 index 000000000..80f610687 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/ServiceTest.kt @@ -0,0 +1,62 @@ +package web5.sdk.dids.didcore + +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals + +class ServiceTest { + + @Test + fun `Builder works`() { + val service = Service.Builder() + .id("did:example:123#key-1") + .type("PFI") + .serviceEndpoint(listOf("https://example.com/")) + .build() + assertEquals("did:example:123#key-1", service.id) + assertEquals("PFI", service.type) + assertEquals(listOf("https://example.com/"), service.serviceEndpoint) + assertEquals( + "Service(" + + "id='did:example:123#key-1', " + + "type='PFI', " + + "serviceEndpoint=[https://example.com/])", + service.toString() + ) + } + + @Test + fun `build() throws exception if id is not set`() { + + assertThrows { + Service.Builder() + .type("PFI") + .serviceEndpoint(listOf("https://example.com/")) + .build() + } + } + + @Test + fun `build() throws exception if type is not set`() { + + assertThrows { + Service.Builder() + .id("did:example:123#key-1") + .serviceEndpoint(listOf("https://example.com/")) + .build() + } + } + + @Test + fun `build() throws exception if serviceEndpoint is not set`() { + + assertThrows { + Service.Builder() + .id("did:example:123#key-1") + .type("PFI") + .build() + } + } + + +} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt new file mode 100644 index 000000000..d5a6ce972 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt @@ -0,0 +1,87 @@ +package web5.sdk.dids.didcore + +import com.nimbusds.jose.jwk.JWK +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import web5.sdk.crypto.AlgorithmId +import web5.sdk.crypto.InMemoryKeyManager +import kotlin.test.Test +import kotlin.test.assertEquals + +class VerificationMethodTest { + + var publicKey: JWK? = null + + @BeforeEach + fun setUp() { + val manager = InMemoryKeyManager() + val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) + publicKey = manager.getPublicKey(keyAlias) + } + @Test + fun `Builder works`() { + val vm = VerificationMethod.Builder() + .id("did:example:123#key-1") + .type("IdentityKey") + .controller("did:example:123") + .publicKeyJwk(publicKey!!) + .build() + assertEquals("did:example:123#key-1", vm.id) + assertEquals("IdentityKey", vm.type) + assertEquals("did:example:123", vm.controller) + assertEquals(publicKey, vm.publicKeyJwk) + assertEquals( + "VerificationMethod(" + + "id='did:example:123#key-1', " + + "type='IdentityKey', " + + "controller='did:example:123', " + + "publicKeyJwk=$publicKey)", + vm.toString() + ) + } + + @Test + fun `build() throws exception if id is not set`() { + assertThrows { + VerificationMethod.Builder() + .type("IdentityKey") + .controller("did:example:123") + .publicKeyJwk(publicKey!!) + .build() + } + } + + @Test + fun `build() throws exception if type is not set`() { + assertThrows { + VerificationMethod.Builder() + .id("did:example:123#key-1") + .controller("did:example:123") + .publicKeyJwk(publicKey!!) + .build() + } + } + + @Test + fun `build() throws exception if controller is not set`() { + assertThrows { + VerificationMethod.Builder() + .id("did:example:123#key-1") + .type("IdentityKey") + .publicKeyJwk(publicKey!!) + .build() + } + } + + @Test + fun `build() throws exception if publicKeyJwk is not set`() { + assertThrows { + VerificationMethod.Builder() + .id("did:example:123#key-1") + .type("IdentityKey") + .controller("did:example:123") + .build() + } + } + +} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt index b1e780c36..f68844d41 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt @@ -137,14 +137,15 @@ class DhtTest { @Test fun `create and parse a bep44 put request`() { val manager = InMemoryKeyManager() - val did = DidDht.create(manager) + val diddht = DidDhtApi {} + val did = diddht.create(manager) require(did.didDocument != null) - val kid = did.didDocument!!.verificationMethods?.first()?.publicKeyJwk?.get("kid")?.toString() + val kid = did.didDocument!!.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() assertNotNull(kid) - val message = did.didDocument?.let { DidDht.toDnsPacket(it) } + val message = did.didDocument?.let { diddht.toDnsPacket(it) } assertNotNull(message) val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) @@ -158,74 +159,46 @@ class DhtTest { @Test fun `put and get a bep44 message to a pkarr relay`() { - val dht = DhtClient(engine = mockEngine()) + val dhtClient = DhtClient() val manager = InMemoryKeyManager() - val did = DidDht.create(manager) + val diddht = DidDhtApi {} + val did = diddht.create(manager) require(did.didDocument != null) - val kid = did.didDocument!!.verificationMethods?.first()?.publicKeyJwk?.get("kid")?.toString() + val kid = did.didDocument!!.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() assertNotNull(kid) - val message = did.didDocument?.let { DidDht.toDnsPacket(it) } + val message = did.didDocument?.let { diddht.toDnsPacket(it) } assertNotNull(message) val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) assertNotNull(bep44Message) - assertDoesNotThrow { dht.pkarrPut(did.suffix(), bep44Message) } + assertDoesNotThrow { dhtClient.pkarrPut(did.suffix(), bep44Message) } - val retrievedMessage = assertDoesNotThrow { dht.pkarrGet(did.suffix()) } + val retrievedMessage = assertDoesNotThrow { dhtClient.pkarrGet(did.suffix()) } assertNotNull(retrievedMessage) } @Test fun `bad pkarr put`() { - val dht = DhtClient(engine = mockEngine()) - val manager = InMemoryKeyManager() - val did = DidDht.create(manager) - - require(did.didDocument != null) - - val kid = did.didDocument!!.verificationMethods?.first()?.publicKeyJwk?.get("kid")?.toString() - assertNotNull(kid) - - val message = did.didDocument?.let { DidDht.toDnsPacket(it) } - assertNotNull(message) - - val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) - assertNotNull(bep44Message) + val bep = Bep44Message( + v = "v".toByteArray(), + sig = "s".repeat(64).toByteArray(), + k = "k".repeat(32).toByteArray(), + seq = 1 + ) - val exception = assertThrows { dht.pkarrPut("bad", bep44Message) } + val exception = assertThrows { DhtClient().pkarrPut("bad", bep) } assertEquals("Identifier must be a z-base-32 encoded Ed25519 public key", exception.message) } @Test fun `bad pkarr get`() { - val dht = DhtClient(engine = mockEngine()) - val exception = assertThrows { dht.pkarrGet("bad") } + val exception = assertThrows { DhtClient().pkarrGet("bad") } assertEquals("Identifier must be a z-base-32 encoded Ed25519 public key", exception.message) } - - @OptIn(ExperimentalStdlibApi::class) - private fun mockEngine() = MockEngine { request -> - val hexResponse = "1ad37b5b8ed6c5fc87b64fe4849d81e7446c31b36138d03b9f6d68837123d6ae6aedf91e0340a7c83cd53b95a60" + - "0ffe4a2264c3c677d7d16ca6bd30e05fa820c00000000659dd40e000004000000000200000000035f6b30045f646964000010000100" + - "001c2000373669643d303b743d303b6b3d63506262357357792d553547333854424a79504d6f4b714632746f4c563563395a3177484" + - "56b7448764c6fc0100010000100001c20002322766d3d6b303b617574683d6b303b61736d3d6b303b696e763d6b303b64656c3d6b30" - - when { - request.url.encodedPath == "/" && request.method == HttpMethod.Put -> { - respond("Success", HttpStatusCode.OK) - } - - request.url.encodedPath.matches("/\\w+".toRegex()) && request.method == HttpMethod.Get -> { - respond(hexResponse.hexToByteArray(), HttpStatusCode.OK) - } - - else -> respond("Success", HttpStatusCode.OK) - } - } } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 29739cf94..1a8f7afa1 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -1,13 +1,12 @@ package web5.sdk.dids.methods.dht import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.node.ObjectNode import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import foundation.identity.did.DIDDocument -import foundation.identity.did.Service -import foundation.identity.did.parser.ParserException import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpMethod @@ -20,17 +19,22 @@ import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.doReturn import org.mockito.kotlin.spy import org.mockito.kotlin.whenever +import web5.sdk.common.Json import web5.sdk.common.ZBase32 import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.PublicKeyPurpose +import web5.sdk.dids.JwkDeserializer +import web5.sdk.dids.PurposesDeserializer +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.didcore.Service import web5.sdk.dids.exceptions.InvalidIdentifierException import web5.sdk.dids.exceptions.InvalidIdentifierSizeException import web5.sdk.dids.exceptions.InvalidMethodNameException +import web5.sdk.dids.exceptions.ParserException import web5.sdk.testing.TestVectors import java.io.File -import java.net.URI import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -83,7 +87,7 @@ class DidDhtTest { DidDht.validateIdentityKey("did:dht:1bxdi3tbf1ud6cpk3ef9pz83erk9c6mmh877qfhfcd7ppzbgh7co7", manager) } assertEquals( - "expected size of decoded identifier \"1bxdi3tbf1ud6cpk3ef9pz83erk9c6mmh877qfhfcd7ppzbgh7co7\" to be 32", + "expected size of decoded identifier 1bxdi3tbf1ud6cpk3ef9pz83erk9c6mmh877qfhfcd7ppzbgh7co7 to be 32", exception.message ) } @@ -100,14 +104,14 @@ class DidDhtTest { assertDoesNotThrow { did.validate() } assertNotNull(did) assertNotNull(did.didDocument) - assertEquals(1, did.didDocument!!.verificationMethods.size) - assertContains(did.didDocument!!.verificationMethods[0].id.toString(), "#0") - assertEquals(1, did.didDocument!!.assertionMethodVerificationMethods.size) - assertEquals(1, did.didDocument!!.authenticationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityDelegationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityInvocationVerificationMethods.size) - assertNull(did.didDocument!!.keyAgreementVerificationMethods) - assertNull(did.didDocument!!.services) + assertEquals(1, did.didDocument!!.verificationMethod?.size) + assertContains(did.didDocument!!.verificationMethod?.get(0)?.id!!, "#0") + assertEquals(1, did.didDocument!!.assertionMethod?.size) + assertEquals(1, did.didDocument!!.authentication?.size) + assertEquals(1, did.didDocument!!.capabilityDelegation?.size) + assertEquals(1, did.didDocument!!.capabilityInvocation?.size) + assertNull(did.didDocument!!.keyAgreement) + assertNull(did.didDocument!!.service) } @Test @@ -117,24 +121,24 @@ class DidDhtTest { val otherKey = manager.generatePrivateKey(AlgorithmId.secp256k1) val publicKeyJwk = manager.getPublicKey(otherKey).toPublicJWK() val publicKeyJwk2 = ECKeyGenerator(Curve.P_256).generate().toPublicJWK() - val verificationMethodsToAdd: Iterable, String?>> = listOf( + val verificationMethodsToAdd: Iterable, String?>> = listOf( Triple( publicKeyJwk, - arrayOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD), + listOf(Purpose.Authentication, Purpose.AssertionMethod), "did:web:tbd.website" ), Triple( publicKeyJwk2, - arrayOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD), + listOf(Purpose.Authentication, Purpose.AssertionMethod), "did:web:tbd.website" ) ) val serviceToAdd = - Service.builder() - .id(URI("test-service")) + Service.Builder() + .id("test-service") .type("HubService") - .serviceEndpoint("https://example.com/service)") + .serviceEndpoint(listOf("https://example.com/service)")) .build() val opts = CreateDidDhtOptions( @@ -144,15 +148,15 @@ class DidDhtTest { assertNotNull(did) assertNotNull(did.didDocument) - assertEquals(3, did.didDocument!!.verificationMethods.size) - assertEquals(3, did.didDocument!!.assertionMethodVerificationMethods.size) - assertEquals(3, did.didDocument!!.authenticationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityDelegationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityInvocationVerificationMethods.size) - assertNull(did.didDocument!!.keyAgreementVerificationMethods) - assertNotNull(did.didDocument!!.services) - assertEquals(1, did.didDocument!!.services.size) - assertContains(did.didDocument!!.services[0].id.toString(), "test-service") + assertEquals(3, did.didDocument!!.verificationMethod?.size) + assertEquals(3, did.didDocument!!.assertionMethod?.size) + assertEquals(3, did.didDocument!!.authentication?.size) + assertEquals(1, did.didDocument!!.capabilityDelegation?.size) + assertEquals(1, did.didDocument!!.capabilityInvocation?.size) + assertNull(did.didDocument!!.keyAgreement) + assertNotNull(did.didDocument!!.service) + assertEquals(1, did.didDocument!!.service?.size) + assertContains(did.didDocument!!.service?.get(0)?.id!!, "test-service") } @Test @@ -172,7 +176,7 @@ class DidDhtTest { assertNotNull(docTypesPair) assertNotNull(docTypesPair.first) assertNotNull(docTypesPair.second) - assertEquals(did.didDocument, docTypesPair.first) + assertEquals(did.didDocument.toString(), docTypesPair.first.toString()) assertEquals(indexes, docTypesPair.second) } @@ -184,14 +188,14 @@ class DidDhtTest { assertNotNull(did) assertNotNull(did.didDocument) - assertEquals(1, did.didDocument!!.verificationMethods.size) - assertContains(did.didDocument!!.verificationMethods[0].id.toString(), "#0") - assertEquals(1, did.didDocument!!.assertionMethodVerificationMethods.size) - assertEquals(1, did.didDocument!!.authenticationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityDelegationVerificationMethods.size) - assertEquals(1, did.didDocument!!.capabilityInvocationVerificationMethods.size) - assertNull(did.didDocument!!.keyAgreementVerificationMethods) - assertNull(did.didDocument!!.services) + assertEquals(1, did.didDocument!!.verificationMethod?.size) + assertContains(did.didDocument!!.verificationMethod?.get(0)?.id!!, "#0") + assertEquals(1, did.didDocument!!.assertionMethod?.size) + assertEquals(1, did.didDocument!!.authentication?.size) + assertEquals(1, did.didDocument!!.capabilityDelegation?.size) + assertEquals(1, did.didDocument!!.capabilityInvocation?.size) + assertNull(did.didDocument!!.keyAgreement) + assertNull(did.didDocument!!.service) } @Test @@ -204,7 +208,7 @@ class DidDhtTest { val result = api.resolve(knownDid) assertNotNull(result) assertNotNull(result.didDocument) - assertEquals(knownDid, result.didDocument!!.id.toString()) + assertEquals(knownDid, result.didDocument!!.id) } } @@ -231,6 +235,7 @@ class DidDhtTest { @Nested inner class DnsPacketTest { + @Test fun `to and from DNS packet - simple DID`() { val manager = InMemoryKeyManager() @@ -241,7 +246,7 @@ class DidDhtTest { val packet = DidDht.toDnsPacket(did.didDocument!!) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id.toString(), packet) + val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) @@ -259,7 +264,7 @@ class DidDhtTest { val packet = DidDht.toDnsPacket(did.didDocument!!, indexes) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id.toString(), packet) + val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) assertNotNull(didFromPacket.second) @@ -274,12 +279,12 @@ class DidDhtTest { val otherKey = manager.generatePrivateKey(AlgorithmId.secp256k1) val publicKeyJwk = manager.getPublicKey(otherKey).toPublicJWK() - val verificationMethodsToAdd: Iterable, String?>> = listOf( - Triple(publicKeyJwk, arrayOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD), null) + val verificationMethodsToAdd: Iterable, String?>> = listOf( + Triple(publicKeyJwk, listOf(Purpose.Authentication, Purpose.AssertionMethod), null) ) - val serviceToAdd = Service.builder() - .id(URI("test-service")) + val serviceToAdd = Service.Builder() + .id("test-service") .type("HubService") .serviceEndpoint(listOf("https://example.com/service", "https://example.com/service2")) .build() @@ -298,7 +303,7 @@ class DidDhtTest { val packet = DidDht.toDnsPacket(did.didDocument!!) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id.toString(), packet) + val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) @@ -337,7 +342,7 @@ class DidDhtTest { } } -private val mapper = jacksonObjectMapper() +private val mapper = Json.jsonMapper class Web5TestVectorsDidDht { data class CreateTestInput( @@ -353,8 +358,10 @@ class Web5TestVectorsDidDht { ) data class VerificationMethodInput( - val jwk: Map, - val purposes: List + @JsonDeserialize(using = JwkDeserializer::class) + val jwk: JWK, + @JsonDeserialize(using = PurposesDeserializer::class) + val purposes: List ) @@ -369,9 +376,9 @@ class Web5TestVectorsDidDht { doReturn(identityKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.Ed25519) val verificationMethods = vector.input.additionalVerificationMethods?.map { verificationMethodInput -> - val jwk = JWK.parse(verificationMethodInput.jwk) - Triple(jwk, verificationMethodInput.purposes.toTypedArray(), null) - } + Triple(verificationMethodInput.jwk, verificationMethodInput.purposes.toList(), null) + }?.asIterable() + val options = CreateDidDhtOptions( verificationMethods = verificationMethods, publish = false, @@ -381,13 +388,19 @@ class Web5TestVectorsDidDht { ) val didDht = DidDht.create(keyManager, options) assertEquals( - JsonCanonicalizer(vector.output?.toJson()).encodedString, - JsonCanonicalizer(didDht.didDocument!!.toCustomJson()).encodedString, + JsonCanonicalizer(Json.stringify(vector.output!!)).encodedString, + JsonCanonicalizer(Json.stringify(didDht.didDocument!!)).encodedString, vector.description ) } } + @Test + fun `resolve fails when identifier size is incorrect`() { + val result = DidDht.resolve("did:dht:foo") + assertEquals("invalidDid", result.didResolutionMetadata.error) + } + @Test fun resolve() { val typeRef = object : TypeReference>() {} @@ -407,14 +420,4 @@ class Web5TestVectorsDidDht { assertEquals(vector.output, result, vector.description) } } -} - -// The test vectors assume the property "controller" is rendered as a string (vs. an array of strings) when there is -// only one controller. -private fun DIDDocument.toCustomJson(): String? { - val jsonObject = this.jsonObject.toMutableMap() - if (jsonObject["controller"] is List<*> && (jsonObject["controller"] as List<*>).size == 1) { - jsonObject["controller"] = (jsonObject["controller"] as List<*>).single() - } - return mapper.writeValueAsString(jsonObject) -} +} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/ion/DidIonTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/ion/DidIonTest.kt deleted file mode 100644 index 30ed71c98..000000000 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/ion/DidIonTest.kt +++ /dev/null @@ -1,592 +0,0 @@ -package web5.sdk.dids.methods.ion - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.client.engine.mock.toByteArray -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.content.OutputStreamContent -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel -import org.apache.commons.codec.binary.Hex -import org.erdtman.jcs.JsonCanonicalizer -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.spy -import org.mockito.kotlin.whenever -import web5.sdk.crypto.AlgorithmId -import web5.sdk.crypto.AwsKeyManager -import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.PublicKeyPurpose -import web5.sdk.dids.methods.ion.models.PublicKey -import web5.sdk.dids.methods.ion.models.Service -import web5.sdk.dids.methods.ion.models.SidetreeCreateOperation -import web5.sdk.dids.methods.ion.models.SidetreeUpdateOperation -import web5.sdk.dids.methods.util.readKey -import java.io.File -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class DidIonTest { - - @Test - @Ignore("For demonstration purposes only - this makes a network call") - fun createWithDefault() { - val did = DidIon.create(InMemoryKeyManager()) - assertContains(did.uri, "did:ion:") - assertTrue(did.creationMetadata!!.longFormDid.startsWith(did.uri)) - } - - @Test - @Ignore("For demonstration purposes only - relies on network and AWS configuration") - fun `create with AWS key manager`() { - val keyManager = AwsKeyManager() - val ionManager = DidIon - val didsCreated = buildList { - repeat(32) { - add(ionManager.create(keyManager)) - } - } - didsCreated.forEach { println(it.uri) } - } - - @Test - fun `exceptions are thrown when attempting ops on non-anchored did`() { - val did = DidIon.create(InMemoryKeyManager()) - assertThrows { - did.update(UpdateDidIonOptions(did.creationMetadata?.keyAliases?.updateKeyAlias!!)) - } - assertThrows { - did.deactivate(DeactivateDidIonOptions(did.creationMetadata?.keyAliases?.recoveryKeyAlias!!)) - } - assertThrows { - did.recover(RecoverDidIonOptions(did.creationMetadata?.keyAliases?.recoveryKeyAlias!!)) - } - } - - @Test - fun `invalid charset verificationMethodId throws exception`() { - val verificationKey = readKey("src/test/resources/verification_jwk.json") - - val exception = assertThrows { - DidIon.create( - InMemoryKeyManager(), - CreateDidIonOptions( - verificationMethodsToAdd = listOf( - JsonWebKey2020VerificationMethod( - id = "space is not part of the base64 url chars", - publicKeyJwk = verificationKey - ) - ), - ) - ) - } - assertContains(exception.message!!, "is not base 64 url charset") - } - - @Test - fun `invalid services throw exception`() { - class TestCase( - val service: Service, - val expectedContains: String - ) - - val testCases = listOf( - TestCase( - Service( - id = "#dwn", - type = "DWN", - serviceEndpoint = "http://my.service.com", - ), - "is not base 64 url charse", - ), - TestCase( - Service( - id = "dwn", - type = "really really really really really really really really long type", - serviceEndpoint = "http://my.service.com", - ), - "service type \"really really really really really really really really long type\" exceeds" + - " max allowed length of 30", - ), - TestCase( - Service( - id = "dwn", - type = "DWN", - serviceEndpoint = "an invalid uri", - ), - "service endpoint is not a valid URI", - ) - ) - for (testCase in testCases) { - val exception = assertThrows { - DidIon.create( - InMemoryKeyManager(), - CreateDidIonOptions( - servicesToAdd = listOf(testCase.service) - ) - ) - } - assertContains(exception.message!!, testCase.expectedContains) - } - } - - @Test - fun `very long verificationMethodId throws exception`() { - val verificationKey = readKey("src/test/resources/verification_jwk.json") - - val exception = assertThrows { - DidIon.create( - InMemoryKeyManager(), - CreateDidIonOptions( - verificationMethodsToAdd = listOf( - JsonWebKey2020VerificationMethod( - id = "something_thats_really_really_really_really_really_really_long", - publicKeyJwk = verificationKey - ) - ), - ) - ) - } - assertContains(exception.message!!, "exceeds max allowed length") - } - - @Test - fun createWithCustom() { - val keyManager = spy(InMemoryKeyManager()) - val verificationKey = readKey("src/test/resources/verification_jwk.json") - val updateKey = readKey("src/test/resources/update_jwk.json") - val updateKeyId = keyManager.import(updateKey) - doReturn(updateKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.secp256k1) - - val recoveryKey = readKey("src/test/resources/recovery_jwk.json") - val recoveryKeyId = keyManager.import(recoveryKey) - doReturn(recoveryKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.secp256k1) - - val didIonApi = DidIonApi { - ionHost = "madeuphost" - engine = mockEngine() - } - val opts = CreateDidIonOptions( - verificationMethodsToAdd = listOf( - JsonWebKey2020VerificationMethod( - id = verificationKey.keyID, - publicKeyJwk = verificationKey, - relationships = listOf(PublicKeyPurpose.AUTHENTICATION), - ) - ), - servicesToAdd = listOf( - Service( - id = "dwn", - type = "DWN", - serviceEndpoint = "http://hub.my-personal-server.com", - ) - ), - ) - val did = didIonApi.create(keyManager, opts) - assertContains(did.uri, "did:ion:") - assertContains(did.creationMetadata!!.longFormDid, did.creationMetadata!!.shortFormDid) - } - - @Test - fun `serializing and deserializing produces the same create operation`() { - val jsonContent = File("src/test/resources/create_operation.json").readText() - val expectedContent = JsonCanonicalizer(jsonContent).encodedString - - val mapper = jacksonObjectMapper() - val createOperation = mapper.readValue(jsonContent) - - val jsonString = mapper.writeValueAsString(createOperation) - assertEquals(expectedContent, JsonCanonicalizer(jsonString).encodedString) - } - - @Test - fun `method name is ion`() { - assertEquals("ion", DidIon.methodName) - } - - @Test - fun `create changes the key manager state`() { - val keyManager = InMemoryKeyManager() - val did = DidIonApi { - engine = mockEngine() - }.create( - keyManager, CreateDidIonOptions( - verificationMethodsToAdd = listOf( - VerificationMethodCreationParams( - AlgorithmId.secp256k1, - relationships = listOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.ASSERTION_METHOD) - ), - VerificationMethodCreationParams( - AlgorithmId.secp256k1, - relationships = listOf(PublicKeyPurpose.ASSERTION_METHOD) - ), - ) - ) - ) - val metadata = did.creationMetadata!! - - assertContains(did.uri, "did:ion:") - assertContains(metadata.longFormDid, metadata.shortFormDid) - assertEquals(2, metadata.keyAliases.verificationKeyAliases.size) - assertDoesNotThrow { - keyManager.getPublicKey(metadata.keyAliases.recoveryKeyAlias!!) - keyManager.getPublicKey(metadata.keyAliases.updateKeyAlias!!) - metadata.keyAliases.verificationKeyAliases.forEach(keyManager::getPublicKey) - } - } - - @Test - fun `update throws exception when given invalid input`() { - val keyManager = InMemoryKeyManager() - val keyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - val publicKey = keyManager.getPublicKey(keyAlias) - - val updateKeyAlias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) - - class TestCase( - val services: Iterable = emptyList(), - val publicKeys: Iterable = emptyList(), - val expected: String - ) - - val testCases = arrayOf( - TestCase( - services = listOf( - Service( - id = "#dwn", - type = "DWN", - serviceEndpoint = "http://my.service.com", - ) - ), - expected = "id \"#dwn\" is not base 64 url charset", - ), - TestCase( - publicKeys = listOf( - JsonWebKey2020VerificationMethod( - id = "#publicKey1", - publicKeyJwk = publicKey, - ) - ), - expected = "id \"#publicKey1\" is not base 64 url charset", - ), - TestCase( - publicKeys = listOf( - JsonWebKey2020VerificationMethod( - id = "publicKey1", - publicKeyJwk = publicKey, - ), - - JsonWebKey2020VerificationMethod( - id = "publicKey1", - publicKeyJwk = publicKey, - ) - ), - expected = "DID Document key with ID \"publicKey1\" already exists.", - ), - TestCase( - publicKeys = listOf( - JsonWebKey2020VerificationMethod( - id = "publicKey1", - publicKeyJwk = publicKey, - relationships = listOf(PublicKeyPurpose.AUTHENTICATION, PublicKeyPurpose.AUTHENTICATION) - ) - ), - expected = "Public key purpose \"authentication\" already specified.", - ), - ) - for (testCase in testCases) { - val result = assertThrows { - DidIon.update( - keyManager, - "did:ion:123", - UpdateDidIonOptions( - updateKeyAlias = updateKeyAlias, - servicesToAdd = testCase.services, - verificationMethodsToAdd = testCase.publicKeys, - ) - ) - } - assertEquals(testCase.expected, result.message) - } - } - - @Test - fun `update fails when update key is absent`() { - val result = assertThrows { - DidIon.update( - InMemoryKeyManager(), - "did:ion:123", - UpdateDidIonOptions( - updateKeyAlias = "my_fake_key", - ) - ) - } - assertEquals("key with alias my_fake_key not found", result.message) - } - - @Test - fun `create sends the expected operation`() { - val mapper = jacksonObjectMapper() - - val verificationMethod1 = publicKey1VerificationMethod(mapper) - val service: Service = mapper.readValue(File("src/test/resources/service1.json").readText()) - - val keyManager = spy(InMemoryKeyManager()) - - val recoveryKey = readKey("src/test/resources/jwkEs256k1Public.json") - val recoveryKeyAlias = keyManager.import(recoveryKey) - - val nextUpdateKey = readKey("src/test/resources/jwkEs256k2Public.json") - val nextUpdateKeyId = keyManager.import(nextUpdateKey) - - doReturn(nextUpdateKeyId, recoveryKeyAlias).whenever(keyManager).generatePrivateKey(AlgorithmId.secp256k1) - - val (result, _) = DidIon.createOperation( - keyManager, - CreateDidIonOptions( - verificationMethodsToAdd = listOf(verificationMethod1), - servicesToAdd = listOf(service), - ) - ) - - assertEquals("create", result.type) - assertEquals("EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA", result.delta.updateCommitment.toBase64Url()) - assertEquals(1, result.delta.patches.count()) - assertEquals("EiBfOZdMtU6OBw8Pk879QtZ-2J-9FbbjSZyoaA_bqD4zhA", result.suffixData.recoveryCommitment.toBase64Url()) - assertEquals("EiCfDWRnYlcD9EGA3d_5Z1AHu-iYqMbJ9nfiqdz5S8VDbg", result.suffixData.deltaHash) - } - - private fun publicKey1VerificationMethod(mapper: ObjectMapper): EcdsaSecp256k1VerificationKey2019VerificationMethod { - val publicKey1: PublicKey = mapper.readValue( - File("src/test/resources/publicKeyModel1.json").readText() - ) - return EcdsaSecp256k1VerificationKey2019VerificationMethod( - id = publicKey1.id, - controller = publicKey1.controller, - publicKeyJwk = publicKey1.publicKeyJwk, - relationships = publicKey1.purposes, - ) - } - - @Test - fun `update sends the expected operation`() { - val mapper = jacksonObjectMapper() - - val keyManager: InMemoryKeyManager = spy(InMemoryKeyManager()) - - val updateKey = readKey("src/test/resources/jwkEs256k1Private.json") - val updateKeyId = keyManager.import(updateKey) - - val nextUpdateKey = readKey("src/test/resources/jwkEs256k2Public.json") - val nextUpdateKeyId = keyManager.import(nextUpdateKey) - doReturn(nextUpdateKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.secp256k1) - - val service: Service = mapper.readValue(File("src/test/resources/service1.json").readText()) - val publicKey1 = publicKey1VerificationMethod(mapper) - - val validationMockEngine = MockEngine { request -> - val updateOp: SidetreeUpdateOperation = mapper.readValue((request.body as OutputStreamContent).toByteArray()) - assertEquals("EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", updateOp.didSuffix) - assertEquals("update", updateOp.type) - assertEquals("EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ", updateOp.revealValue.toBase64Url()) - assertEquals( - "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoibklxbFJDeDB" + - "leUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdh" + - "Q004RjdrIn0sImRlbHRhSGFzaCI6IkVpQXZsbVVRYy1jaDg0Slp5bmdQdkJzUkc3eWh4aUFSenlYOE5lNFQ4LTlyTncifQ." + - "Q9MuoQqFlhYhuLDgx4f-0UM9QyCfZp_cXt7vnQ4ict5P4_ZWKwG4OXxxqFvdzE-e3ZkEbvfR0YxEIpYO9MrPFw", - updateOp.signedData - ) - assertEquals("EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA", updateOp.delta.updateCommitment.toBase64Url()) - assertEquals(4, updateOp.delta.patches.count()) - respond( - content = ByteReadChannel("""{"hello":"world"}"""), - headers = headersOf(HttpHeaders.ContentType, "application/json"), - status = HttpStatusCode.OK, - ) - } - val updateMetadata = DidIonApi { - engine = validationMockEngine - }.update( - keyManager, - "did:ion:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", - UpdateDidIonOptions( - updateKeyAlias = updateKeyId, - servicesToAdd = listOf(service), - idsOfServicesToRemove = setOf("someId1"), - verificationMethodsToAdd = listOf(publicKey1), - idsOfPublicKeysToRemove = setOf("someId2"), - ), - ) - - assertFalse(updateMetadata.keyAliases.updateKeyAlias.isNullOrEmpty()) - assertEquals("""{"hello":"world"}""", updateMetadata.operationsResponseBody) - } - - @Test - fun `recover operation is the expected one`() { - val mapper = jacksonObjectMapper() - - val publicKey1 = publicKey1VerificationMethod(mapper) - val service: Service = mapper.readValue(File("src/test/resources/service1.json").readText()) - - val keyManager = spy(InMemoryKeyManager()) - val recoveryKey = readKey("src/test/resources/jwkEs256k1Private.json") - val recoveryKeyAlias = keyManager.import(recoveryKey) - - val nextRecoveryKey = readKey("src/test/resources/jwkEs256k2Public.json") - val nextRecoveryKeyId = keyManager.import(nextRecoveryKey) - - val nextUpdateKey = readKey("src/test/resources/jwkEs256k3Public.json") - val nextUpdateKeyId = keyManager.import(nextUpdateKey) - - doReturn(nextRecoveryKeyId, nextUpdateKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.secp256k1) - - val (recoverOperation, keyAliases) = DidIon.createRecoverOperation( - keyManager, - "did:ion:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", - RecoverDidIonOptions( - recoveryKeyAlias = recoveryKeyAlias, - verificationMethodsToAdd = listOf(publicKey1), - servicesToAdd = listOf(service), - ) - ) - - assertEquals("EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", recoverOperation.didSuffix) - assertEquals("EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ", recoverOperation.revealValue.toBase64Url()) - assertEquals("recover", recoverOperation.type) - assertEquals( - "EiBJGXo0XUiqZQy0r-fQUHKS3RRVXw5nwUpqGVXEGuTs-g", - recoverOperation.delta.updateCommitment.toBase64Url() - ) - assertEquals( - "eyJhbGciOiJFUzI1NksifQ.eyJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURLSWt3cU82OUlQRzNwT2xIa2RiODZuWXQwYU54U" + - "0hadTJyLWJoRXpuamRBIiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoibklxbFJDeDBleUJT" + - "WGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ00" + - "4RjdrIn0sImRlbHRhSGFzaCI6IkVpQm9HNlFtamlTSm5ON2phaldnaV9vZDhjR3dYSm9Nc2RlWGlWWTc3NXZ2SkEifQ.58n6Fel9DmR" + - "AXxwcJMUwYaUhmj5kigKMNrGjr7eJaJcjOmjvwlKLSjiovWiYrb9yjkfMAjpgbAdU_2EDI1_lZw", - recoverOperation.signedData - ) - assertEquals(1, recoverOperation.delta.patches.count()) - assertEquals(nextUpdateKeyId, keyAliases.updateKeyAlias) - assertEquals(nextRecoveryKeyId, keyAliases.recoveryKeyAlias) - } - - @Test - fun `recover creates keys in key manager`() { - val ionManager = DidIonApi { - engine = mockEngine() - } - val keyManager = spy(InMemoryKeyManager()) - val did = ionManager.create(keyManager) - assertNotNull(did.creationMetadata) - val recoveryKeyAlias = did.creationMetadata!!.keyAliases.recoveryKeyAlias - - assertNotNull(recoveryKeyAlias) - // Imagine that your update key was compromised, so you need to recover your DID. - val opts = RecoverDidIonOptions( - recoveryKeyAlias = recoveryKeyAlias, - ) - val recoverResult = ionManager.recover(keyManager, did.uri, opts) - assertNotNull(recoverResult.keyAliases.updateKeyAlias) - assertNotNull(recoverResult.keyAliases.recoveryKeyAlias) - assertNotNull(recoverResult.keyAliases.verificationKeyAliases) - - assertDoesNotThrow { - keyManager.getPublicKey(recoverResult.keyAliases.updateKeyAlias!!) - keyManager.getPublicKey(recoverResult.keyAliases.recoveryKeyAlias!!) - recoverResult.keyAliases.verificationKeyAliases.forEach(keyManager::getPublicKey) - } - assertEquals("{}", recoverResult.operationsResponse) - assertNotEquals(recoveryKeyAlias, recoverResult.keyAliases.recoveryKeyAlias) - } - - @Test - fun `deactivate operation is the expected one`() { - val keyManager = InMemoryKeyManager() - val recoveryKey = readKey("src/test/resources/jwkEs256k1Private.json") - val recoveryKeyAlias = keyManager.import(recoveryKey) - - val deactivateResult = DidIonApi { - engine = mockEngine() - }.deactivate( - keyManager, - "did:ion:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", - DeactivateDidIonOptions( - recoveryKeyAlias = recoveryKeyAlias, - ) - ) - - val deactivateOperation = deactivateResult.deactivateOperation - assertEquals("EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", deactivateOperation.didSuffix) - assertEquals("deactivate", deactivateOperation.type) - assertEquals( - "EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ", - deactivateOperation.revealValue.toBase64Url() - ) - assertEquals( - "eyJhbGciOiJFUzI1NksifQ.eyJkaWRTdWZmaXgiOiJFaUR5T1FiYlpBYTNhaVJ6ZUNrVjdMT3gzU0VSampIOTNFWG9JTTNVb040b1" + - "dnIiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaH" + - "dDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn19.uLgnDBmmFzST4VTmd" + - "JcmFKVicF0kQaBqEnRQLbqJydgIg_2oreihCA5sBBIUBlSXwvnA9xdK97ksJGmPQ7asPQ", - deactivateOperation.signedData - ) - } - - @Test - fun `bad request throws exception`() { - val exception = assertThrows { - DidIonApi { - engine = badRequestMockEngine() - }.resolve("did:ion:foobar") - } - - assertEquals(HttpStatusCode.BadRequest.value, exception.statusCode) - } - - @Test - fun `multihash test vector`() { - // test vector taken from: https://multiformats.io/multihash/#sha2-256---256-bits-aka-sha256 - val input = "Merkle–Damgård".toByteArray() - - val mhBytes = multihash(input) - val mhHex = Hex.encodeHexString(mhBytes) - assertEquals("122041dd7b6443542e75701aa98a0c235951a28a0d851b11564d20022ab11d2589a8", mhHex) - } - - private fun badRequestMockEngine() = MockEngine { - respond( - content = ByteReadChannel("""{}"""), - status = HttpStatusCode.BadRequest, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - - private fun mockEngine() = MockEngine { request -> - when (request.url.encodedPath) { - "/operations" -> { - respond( - content = ByteReadChannel("""{}"""), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - - else -> respond( - content = ByteReadChannel(File("src/test/resources/basic_did_resolution.json").readText()), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - } -} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index 7e59bd98e..d6cac26c5 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -9,9 +9,9 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import web5.sdk.common.Convert +import web5.sdk.common.Json import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.crypto.Jwa import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.DidResolvers import web5.sdk.testing.TestVectors @@ -29,11 +29,12 @@ class DidJwkTest { val did = DidJwk.create(manager) val didResolutionResult = DidResolvers.resolve(did.uri) - val verificationMethod = didResolutionResult.didDocument!!.allVerificationMethods[0] + val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) assertNotNull(verificationMethod) - val jwk = JWK.parse(verificationMethod.publicKeyJwk) + val jwk = verificationMethod.publicKeyJwk + assertNotNull(jwk) val keyAlias = did.keyManager.getDeterministicAlias(jwk) val publicKey = did.keyManager.getPublicKey(keyAlias) @@ -77,6 +78,19 @@ class DidJwkTest { @Nested inner class ResolveTest { + + @Test + fun `throws exception if did cannot be parsed`() { + val result = DidJwk.resolve("did:jwk:invalid") + assertEquals("invalidDid", result.didResolutionMetadata.error) + } + + @Test + fun `throws exception if did method is not jwk`() { + val result = DidJwk.resolve("did:example:123") + assertEquals("methodNotSupported", result.didResolutionMetadata.error) + } + @Test fun `private key throws exception`() { val manager = InMemoryKeyManager() @@ -85,15 +99,18 @@ class DidJwkTest { val encodedPrivateJwk = Convert(privateJwk.toJSONString()).toBase64Url(padding = false) val did = "did:jwk:$encodedPrivateJwk" - assertThrows("decoded jwk value cannot be a private key") { DidJwk.resolve(did) } + assertThrows( + "decoded jwk value cannot be a private key" + ) { DidJwk.resolve(did) } } @Test fun `test vector 1`() { // test vector taken from: https://github.com/quartzjer/did-jwk/blob/main/spec.md#p-256 - @Suppress("MaxLineLength") val did = - "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9" + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFa" + + "koydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00Nkdx" + + "RHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9" val result = DidJwk.resolve(did) assertNotNull(result) @@ -101,15 +118,18 @@ class DidJwkTest { assertNotNull(didDocument) val expectedJson = File("src/test/resources/did_jwk_p256_document.json").readText() - assertEquals(JsonCanonicalizer(expectedJson).encodedString, JsonCanonicalizer(didDocument.toJson()).encodedString) + assertEquals( + JsonCanonicalizer(expectedJson).encodedString, + JsonCanonicalizer(Json.stringify(didDocument)).encodedString + ) } @Test fun `test vector 2`() { // test vector taken from: https://github.com/quartzjer/did-jwk/blob/main/spec.md#x25519 - @Suppress("MaxLineLength") val did = - "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9" + "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZY" + + "dDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9" val result = DidJwk.resolve(did) assertNotNull(result) @@ -117,7 +137,10 @@ class DidJwkTest { assertNotNull(didDocument) val expectedJson = File("src/test/resources/did_jwk_x25519_document.json").readText() - assertEquals(JsonCanonicalizer(expectedJson).encodedString, JsonCanonicalizer(didDocument.toJson()).encodedString) + assertEquals( + JsonCanonicalizer(expectedJson).encodedString, + JsonCanonicalizer(Json.stringify(didDocument)).encodedString + ) } } } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index 1b70b6847..a9567755e 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.JWK import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow @@ -28,13 +27,19 @@ class DidKeyTest { val did = DidKey.create(manager) val didResolutionResult = DidResolvers.resolve(did.uri) - val verificationMethod = didResolutionResult.didDocument!!.allVerificationMethods[0] - require(verificationMethod != null) { "no verification method found" } + assertNotNull(didResolutionResult.didDocument) + val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) + + val jwk = verificationMethod?.publicKeyJwk + assertNotNull(jwk) - val jwk = JWK.parse(verificationMethod.publicKeyJwk) val keyAlias = did.keyManager.getDeterministicAlias(jwk) val publicKey = did.keyManager.getPublicKey(keyAlias) + assertNotNull(jwk) + assertNotNull(keyAlias) + assertNotNull(publicKey) + } } @@ -76,18 +81,20 @@ class DidKeyTest { val didDocument = result.didDocument assertNotNull(didDocument) - assertEquals(did, didDocument.id.toString()) - assertEquals(1, didDocument.allVerificationMethods.size) - assertEquals(1, didDocument.assertionMethodVerificationMethods.size) - assertEquals(1, didDocument.authenticationVerificationMethods.size) - assertEquals(1, didDocument.capabilityDelegationVerificationMethods.size) - assertEquals(1, didDocument.capabilityInvocationVerificationMethods.size) - assertEquals(1, didDocument.keyAgreementVerificationMethods.size) - - val verificationMethod = didDocument.verificationMethods.first() + assertEquals(did, didDocument.id) + assertEquals(1, didDocument.verificationMethod?.size) + assertEquals(1, didDocument.assertionMethod?.size) + assertEquals(1, didDocument.authentication?.size) + assertEquals(1, didDocument.capabilityDelegation?.size) + assertEquals(1, didDocument.capabilityInvocation?.size) + assertEquals(1, didDocument.keyAgreement?.size) + + val verificationMethod = didDocument.verificationMethod?.first() + assertNotNull(verificationMethod) + assertEquals( "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS", - verificationMethod.id.toString() + verificationMethod.id ) // Note: cannot run the controller assertion because underlying lib enforces JSON-LD @context @@ -96,7 +103,7 @@ class DidKeyTest { assertEquals("JsonWebKey2020", verificationMethod.type) assertNotNull(verificationMethod.publicKeyJwk) - val publicKeyJwk = JWK.parse(verificationMethod.publicKeyJwk) // validates + val publicKeyJwk = verificationMethod.publicKeyJwk // validates assertTrue(publicKeyJwk is ECKey) assertEquals(publicKeyJwk.algorithm, JWSAlgorithm.ES256K) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt index 23309eb01..7c513187f 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt @@ -56,7 +56,7 @@ class DidWebTest { } for (did in didsToTest) { val result = api.resolve(did) - assertEquals(did, result.didDocument!!.id.toString()) + assertEquals(did, result.didDocument!!.id) } } diff --git a/dids/src/test/resources/did_document_jwkEx256k1Public_assertion.json b/dids/src/test/resources/did_document_jwkEx256k1Public_assertion.json index 6de102a17..a156b6f60 100644 --- a/dids/src/test/resources/did_document_jwkEx256k1Public_assertion.json +++ b/dids/src/test/resources/did_document_jwkEx256k1Public_assertion.json @@ -1,7 +1,6 @@ { "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/jws-2020/v1" + "https://www.w3.org/ns/did/v1" ], "id": "did:web:example-with-verification-method.com", "verificationMethod": [ diff --git a/dids/src/test/resources/did_jwk_p256_document.json b/dids/src/test/resources/did_jwk_p256_document.json index f6e8ba14a..1b22d815b 100644 --- a/dids/src/test/resources/did_jwk_p256_document.json +++ b/dids/src/test/resources/did_jwk_p256_document.json @@ -1,13 +1,12 @@ { "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/jws-2020/v1" + "https://www.w3.org/ns/did/v1" ], "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", "verificationMethod": [ { "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0", - "type": "JsonWebKey2020", + "type": "JsonWebKey", "controller": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", "publicKeyJwk": { "crv": "P-256", diff --git a/dids/src/test/resources/did_jwk_x25519_document.json b/dids/src/test/resources/did_jwk_x25519_document.json index 6e3a5cc69..35f60ffeb 100644 --- a/dids/src/test/resources/did_jwk_x25519_document.json +++ b/dids/src/test/resources/did_jwk_x25519_document.json @@ -1,13 +1,12 @@ { "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/jws-2020/v1" + "https://www.w3.org/ns/did/v1" ], "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", "verificationMethod": [ { "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0", - "type": "JsonWebKey2020", + "type": "JsonWebKey", "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", "publicKeyJwk": { "kty": "OKP", diff --git a/dids/src/test/resources/expected_long_form_did_resolution.json b/dids/src/test/resources/expected_long_form_did_resolution.json index 1afb5a4f8..5c2cf490d 100644 --- a/dids/src/test/resources/expected_long_form_did_resolution.json +++ b/dids/src/test/resources/expected_long_form_did_resolution.json @@ -3,29 +3,22 @@ "didDocument": { "id": "did:ion:EiBjzeKiW7WSyjEGmj-Issed5hMefmM2_HwybK7dSrD_XA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWdfMjQzOGNiMmQiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiVERGUmlOU3dGcG10cTVXX3k5RHhoMDh0ZUJ6SFZLVzlDand1RHR5NllJVSIsInkiOiJfV3c1cDRWcERobDF2aU5JeUtfM1dVbjVidHJ0OGVrejZnN0pHWUZUUUY4In0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkifV0sInNlcnZpY2VzIjpbeyJpZCI6ImxpbmtlZGRvbWFpbnMiLCJzZXJ2aWNlRW5kcG9pbnQiOnsib3JpZ2lucyI6WyJodHRwczovL2xpbmtlZGluLmNvbS8iXX0sInR5cGUiOiJMaW5rZWREb21haW5zIn0seyJpZCI6Imh1YiIsInNlcnZpY2VFbmRwb2ludCI6eyJpbnN0YW5jZXMiOlsiaHR0cHM6Ly9iZXRhLmh1Yi5tc2lkZW50aXR5LmNvbS92MS4wLzU4OWQ1M2I1LWRlZjUtNDIzNS1iNjQyLThlMWM0MWU4YmNhMSJdfSwidHlwZSI6IklkZW50aXR5SHViIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlBbTUtaE5tVnhSUVdOZm13QzZWMTZhN0c3Sm5rTzM4aUdUUFY2N3F4Q3NDdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQjFPQkhnaktjakpkLVFQei1RYWZQbldBemRucmh5WVB2UElfVnJzTm5Xd0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUM2NnNYSGVGMklydnhIS3pCcWVETGwzVENSS2pVZEJsdmRfbkZKMFp3YTRnIn19", "@context": [ - "https://www.w3.org/ns/did/v1", - { - "@base": "did:ion:EiBjzeKiW7WSyjEGmj-Issed5hMefmM2_HwybK7dSrD_XA:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJzaWdfMjQzOGNiMmQiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoiVERGUmlOU3dGcG10cTVXX3k5RHhoMDh0ZUJ6SFZLVzlDand1RHR5NllJVSIsInkiOiJfV3c1cDRWcERobDF2aU5JeUtfM1dVbjVidHJ0OGVrejZnN0pHWUZUUUY4In0sInB1cnBvc2VzIjpbImF1dGhlbnRpY2F0aW9uIiwiYXNzZXJ0aW9uTWV0aG9kIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkifV0sInNlcnZpY2VzIjpbeyJpZCI6ImxpbmtlZGRvbWFpbnMiLCJzZXJ2aWNlRW5kcG9pbnQiOnsib3JpZ2lucyI6WyJodHRwczovL2xpbmtlZGluLmNvbS8iXX0sInR5cGUiOiJMaW5rZWREb21haW5zIn0seyJpZCI6Imh1YiIsInNlcnZpY2VFbmRwb2ludCI6eyJpbnN0YW5jZXMiOlsiaHR0cHM6Ly9iZXRhLmh1Yi5tc2lkZW50aXR5LmNvbS92MS4wLzU4OWQ1M2I1LWRlZjUtNDIzNS1iNjQyLThlMWM0MWU4YmNhMSJdfSwidHlwZSI6IklkZW50aXR5SHViIn1dfX1dLCJ1cGRhdGVDb21taXRtZW50IjoiRWlBbTUtaE5tVnhSUVdOZm13QzZWMTZhN0c3Sm5rTzM4aUdUUFY2N3F4Q3NDdyJ9LCJzdWZmaXhEYXRhIjp7ImRlbHRhSGFzaCI6IkVpQjFPQkhnaktjakpkLVFQei1RYWZQbldBemRucmh5WVB2UElfVnJzTm5Xd0EiLCJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaUM2NnNYSGVGMklydnhIS3pCcWVETGwzVENSS2pVZEJsdmRfbkZKMFp3YTRnIn19" - } + "https://www.w3.org/ns/did/v1" ], "service": [ { "id": "#linkeddomains", "type": "LinkedDomains", - "serviceEndpoint": { - "origins": [ - "https://www.linkedin.com/" - ] - } + "serviceEndpoint": [ + "https://www.linkedin.com/" + ] }, { "id": "#hub", "type": "IdentityHub", - "serviceEndpoint": { - "instances": [ - "https://beta.hub.msidentity.com/v1.0/589d53b5-def5-4235-b642-8e1c41e8bca1" - ] - } + "serviceEndpoint": [ + "https://beta.hub.msidentity.com/v1.0/589d53b5-def5-4235-b642-8e1c41e8bca1" + ] } ], "verificationMethod": [ @@ -49,14 +42,7 @@ ] }, "didDocumentMetadata": { - "method": { - "published": true, - "recoveryCommitment": "EiC66sXHeF2IrvxHKzBqeDLl3TCRKjUdBlvd_nFJ0Zwa4g", - "updateCommitment": "EiBRKW5GwU-Z8GViwsuBdvZ3CYIuMEi7UGZT4Xds-cUT2Q" - }, - "equivalentId": [ - "did:ion:EiBjzeKiW7WSyjEGmj-Issed5hMefmM2_HwybK7dSrD_XA" - ], + "equivalentId": "did:ion:EiBjzeKiW7WSyjEGmj-Issed5hMefmM2_HwybK7dSrD_XA", "canonicalId": "did:ion:EiBjzeKiW7WSyjEGmj-Issed5hMefmM2_HwybK7dSrD_XA" }, "didResolutionMetadata": {} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b8bebd75..1b8f5163d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,6 @@ com_nfeld_jsonpathkt = "2.0.1" com_nimbusds = "9.37.2" com_squareup_okhttp3 = "4.12.0" com_willowtreeapps_assertk = "0.27.0" -decentralized_identity_did_common_java = "1.9.0" dnsjava = "3.5.2" io_github_erdtman_java_json_canonicalization = "1.1" io_github_oshai_kotlin_logging = "6.0.2" @@ -52,7 +51,6 @@ comNfeldJsonpathkt = { module = "com.nfeld.jsonpathkt:jsonpathkt", version.ref = comNimbusdsJoseJwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "com_nimbusds" } comSquareupOkhttp3 = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "com_squareup_okhttp3" } comWillowtreeappsAssertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "com_willowtreeapps_assertk" } -decentralizedIdentityDidCommonJava= { module = "decentralized-identity:did-common-java", version.ref = "decentralized_identity_did_common_java" } dnsJava = { module = "dnsjava:dnsjava", version.ref = "dnsjava" } orgBouncycastleBcprov = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "org_bouncycastle" } orgBouncycastleBcpkix = { module = "org.bouncycastle:bcpkix-jdk15to18", version.ref = "org_bouncycastle" } diff --git a/web5-spec b/web5-spec index 1e498f37f..2cd6e80b6 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 1e498f37f07a1f29c89c10b8a59c7ed9b7d54050 +Subproject commit 2cd6e80b60ac37dfed9dc718d8c94a830e0b76a9