From 86cd0560619e1ceba28436e99f57a432331345c6 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Mon, 30 Oct 2023 21:45:44 -0700 Subject: [PATCH 1/8] replace multiformats libs with handrolled `multihash` function --- dids/build.gradle.kts | 4 +- .../kotlin/web5/sdk/dids/DidIonManager.kt | 67 +++++++++++++------ .../test/kotlin/web5/sdk/dids/DidIonTest.kt | 13 +++- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/dids/build.gradle.kts b/dids/build.gradle.kts index e58824a41..569f0bc72 100644 --- a/dids/build.gradle.kts +++ b/dids/build.gradle.kts @@ -25,8 +25,6 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0") implementation("com.nimbusds:nimbus-jose-jwt:9.34") implementation("com.github.multiformats:java-multibase:1.1.0") - implementation("org.erwinkok.multiformat:multiformat:1.1.0") - implementation("org.erwinkok.result:result-monad:1.4.0") implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") @@ -38,4 +36,6 @@ dependencies { testImplementation(kotlin("test")) testImplementation("io.ktor:ktor-client-mock:$ktor_version") testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") + testImplementation("commons-codec:commons-codec:1.16.0") + } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt b/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt index 360c02a0d..63bbdcf0d 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt @@ -23,11 +23,8 @@ import io.ktor.http.isSuccess import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking import org.erdtman.jcs.JsonCanonicalizer -import org.erwinkok.multiformat.multicodec.Multicodec -import org.erwinkok.multiformat.multihash.Multihash -import org.erwinkok.result.get -import org.erwinkok.result.getOrThrow import web5.sdk.common.Convert +import web5.sdk.common.Varint import web5.sdk.crypto.KeyManager import web5.sdk.dids.ion.model.AddPublicKeysAction import web5.sdk.dids.ion.model.AddServicesAction @@ -52,11 +49,14 @@ import web5.sdk.dids.ion.model.SidetreeRecoverOperation import web5.sdk.dids.ion.model.SidetreeUpdateOperation import web5.sdk.dids.ion.model.UpdateOperationSignedData 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 [DidIonManager]. * @@ -221,9 +221,10 @@ public sealed class DidIonManager( override fun create(keyManager: KeyManager, options: CreateDidIonOptions?): DidIonHandle { val (createOp, keys) = createOperation(keyManager, options) - val shortFormDidSegment = Convert( - Multihash.sum(Multicodec.SHA2_256, canonicalized(createOp.suffixData)).get()?.bytes() - ).toBase64Url(padding = false) + 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, @@ -370,9 +371,10 @@ public sealed class DidIonManager( } private fun deltaHash(updateOpDeltaObject: Delta): String { - val canonicalized = canonicalized(updateOpDeltaObject) - val deltaHashBytes = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow().bytes() - return Base64URL.encode(deltaHashBytes).toString() + val canonicalizedOp = canonicalized(updateOpDeltaObject) + val opMultihash = multihash(canonicalizedOp) + + return Convert(opMultihash).toBase64Url(padding = false) } private fun validateDidDocumentKeys(publicKeys: Iterable) { @@ -514,10 +516,10 @@ public sealed class DidIonManager( recoveryCommitment: Commitment): OperationSuffixDataObject { val jsonString = mapper.writeValueAsString(createOperationDeltaObject) val canonicalized = JsonCanonicalizer(jsonString).encodedUTF8 - val deltaHashBytes = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow().bytes() - val deltaHash = Convert(deltaHashBytes).toBase64Url(padding = false) + val deltaMultihash = multihash(canonicalized) + return OperationSuffixDataObject( - deltaHash = deltaHash, + deltaHash = Convert(deltaMultihash).toBase64Url(padding = false), recoveryCommitment = recoveryCommitment ) } @@ -675,13 +677,16 @@ private fun JWK.commitment(): Commitment { val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 // 3. Use the implementation’s HASH_PROTOCOL to Multihash the canonicalized public key to generate the REVEAL_VALUE, - val mh = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow() - val intermediate = mh.digest + // val mh = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow() + // val intermediate = mh.digest + + val sha256 = MessageDigest.getInstance("SHA-256") + val pkDigest = sha256.digest(canonicalized) // then Multihash the resulting Multihash value again using the implementation’s HASH_PROTOCOL to produce // the public key commitment. - val hashOfHash = Multihash.sum(Multicodec.SHA2_256, intermediate).getOrThrow().bytes() - return Commitment(hashOfHash) + val pkDigestMultihash = multihash(pkDigest) + return Commitment(pkDigestMultihash) } private fun JWK.reveal(): Reveal { @@ -693,8 +698,32 @@ private fun JWK.reveal(): Reveal { val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 // 3. Use the implementation’s HASH_PROTOCOL to Multihash the canonicalized public key to generate the REVEAL_VALUE, - val mh = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow() - return Reveal(mh.bytes()) + 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 } /** diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt index 5f8c0ff85..368dcfad1 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt @@ -12,6 +12,7 @@ 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.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -438,7 +439,7 @@ class DidIonTest { val recoveryKey = readKey("src/test/resources/jwkEs256k1Private.json") val recoveryKeyAlias = keyManager.import(recoveryKey) - val deactivateResult = DidIonManager{ + val deactivateResult = DidIonManager { engine = mockEngine() }.deactivate( keyManager, @@ -475,6 +476,16 @@ class DidIonTest { 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("""{}"""), From 16b2434cf05a0a59b8de3c8a64cfb86fe62e8ce4 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Mon, 30 Oct 2023 21:46:08 -0700 Subject: [PATCH 2/8] set java compatibility to 11 --- build.gradle.kts | 10 +++++----- .../src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a532265a8..35212150b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { } allprojects { - version = "0.0.8" + version = "0.0.9" group = "web5" } @@ -54,9 +54,9 @@ subprojects { kotlin { explicitApi() - jvmToolchain(17) + jvmToolchain(11) compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7) languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7) } @@ -65,8 +65,8 @@ subprojects { java { withJavadocJar() withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } publishing { diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt index 907dd9ca7..e0ca0bdbd 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt @@ -65,7 +65,7 @@ class Secp256k1Test { repeat(10_000) { // generate a payload of up to 100 random bytes - val payloadSize = Random().nextInt(1, 100) + val payloadSize = Random().nextInt(100) + 1 val payload = ByteArray(payloadSize) Random().nextBytes(payload) From 2d4c87471e101dd17257adfcef34bf4e168c4848 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 30 Oct 2023 22:11:29 -0700 Subject: [PATCH 3/8] remove hardcoded encoded list --- .../web5/sdk/credentials/StatusListCredentialTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 0b3d0c279..274890edd 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -164,10 +164,11 @@ class StatusListCredentialTest { statusListCredential.vcDataModel.credentialSubject.jsonObject["statusPurpose"] as? String? ) - assertEquals( - "H4sIAAAAAAAA/2NgQAESAAPT1/8QAAAA", - statusListCredential.vcDataModel.credentialSubject.jsonObject["encodedList"] as? String? - ) + // TODO: Check encoding across other sdks and spec - https://github.com/TBD54566975/web5-kt/issues/97 + // assertEquals( + // "H4sIAAAAAAAA/2NgQAESAAPT1/8QAAAA", + // statusListCredential.vcDataModel.credentialSubject.jsonObject["encodedList"] as? String? + //) } From aad5f1cbabacae0f514945ba3fb94ee26208e130 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Tue, 31 Oct 2023 01:28:06 -0700 Subject: [PATCH 4/8] remove redundant comments and commented out code --- .../main/kotlin/web5/sdk/dids/DidIonManager.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt b/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt index 63bbdcf0d..eba58150d 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidIonManager.kt @@ -670,34 +670,23 @@ private interface VerificationPublicKeyOption { private fun JWK.commitment(): Commitment { require(!this.isPrivate) { throw IllegalArgumentException("provided JWK must not be a private key") } - // 1. Encode the public key into the form of a valid JWK. - val pkJson = this.toJSONString() - // 2. Canonicalize the JWK encoded public key using the implementation’s JSON_CANONICALIZATION_SCHEME. + val pkJson = this.toJSONString() val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 - // 3. Use the implementation’s HASH_PROTOCOL to Multihash the canonicalized public key to generate the REVEAL_VALUE, - // val mh = Multihash.sum(Multicodec.SHA2_256, canonicalized).getOrThrow() - // val intermediate = mh.digest - val sha256 = MessageDigest.getInstance("SHA-256") val pkDigest = sha256.digest(canonicalized) - // then Multihash the resulting Multihash value again using the implementation’s HASH_PROTOCOL to produce - // the public key commitment. 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") } - // 1. Encode the public key into the form of a valid JWK. - val pkJson = this.toJSONString() - // 2. Canonicalize the JWK encoded public key using the implementation’s JSON_CANONICALIZATION_SCHEME. + val pkJson = this.toJSONString() val canonicalized = JsonCanonicalizer(pkJson).encodedUTF8 - // 3. Use the implementation’s HASH_PROTOCOL to Multihash the canonicalized public key to generate the REVEAL_VALUE, val mh = multihash(canonicalized) return Reveal(mh) } From bd049851abd630f793fa8219fcc2d50f182602c7 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Tue, 31 Oct 2023 10:53:39 -0700 Subject: [PATCH 5/8] refactor `export` Co-authored-by: Andres Uribe --- .../main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index b486fea19..33371f9e9 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -125,13 +125,5 @@ public class InMemoryKeyManager : KeyManager { * * @return A list of key representations in map format. */ - public fun export(): List> { - val keySet = mutableListOf>() - for (jwk in keyStore.values) { - val jsonJwk = jwk.toJSONObject() - keySet.add(jsonJwk) - } - - return keySet - } + public fun export(): List> = keyStore.map { it.value.toJSONObject() } } From 471c2a669934db617170b22239119552464b9937 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Tue, 31 Oct 2023 10:54:13 -0700 Subject: [PATCH 6/8] refactor `import` Co-authored-by: Andres Uribe --- .../kotlin/web5/sdk/crypto/InMemoryKeyManager.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index 33371f9e9..7c19b5392 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -92,17 +92,9 @@ public class InMemoryKeyManager : KeyManager { * @param keySet A list of key representations in map format. * @return A list of key aliases belonging to the imported keys. */ - public fun import(keySet: List>): List { - val keyAliases = mutableListOf() - - for (jsonJwk in keySet) { - val jwk = JWK.parse(jsonJwk) - val keyAlias = import(jwk) - - keyAliases.add(keyAlias) - } - - return keyAliases + public fun import(keySet: Iterable>): List = keySet.map { + val jwk = JWK.parse(it) + import(jwk) } /** From 924a5e378d986e29901d0a1dcf98f359d4a0793f Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Tue, 31 Oct 2023 10:55:08 -0700 Subject: [PATCH 7/8] Update test name to be more descriptive Co-authored-by: Kendall Weihe --- dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt index 973b3ccb5..d4fea76d1 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt @@ -81,7 +81,7 @@ class DidKeyTest { @Nested inner class ImportExportTest { @Test - fun `importing and exporting using InMemoryKeyManager works`() { + fun `InMemoryKeyManager export then re-import doesn't throw exception`() { val jsonMapper = ObjectMapper() .registerKotlinModule() .setSerializationInclusion(JsonInclude.Include.NON_NULL) From 12bf4958c1d0057354e15994df983ce6c39f3664 Mon Sep 17 00:00:00 2001 From: Moe Jangda Date: Tue, 31 Oct 2023 10:57:38 -0700 Subject: [PATCH 8/8] remove printlns from test --- dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt index 1c4d5b07e..81b793033 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidKeyTest.kt @@ -94,10 +94,6 @@ class DidKeyTest { val serializedKeySet = jsonMapper.writeValueAsString(keySet) val didUri = did.uri - println(serializedKeySet) - println(didUri) - - val jsonKeySet: List> = jsonMapper.readValue(serializedKeySet) val km2 = InMemoryKeyManager() km2.import(jsonKeySet)