From 4e85e4735686955f6d608f97c3503a9b92f61614 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:31:05 +0100 Subject: [PATCH] feat: signData --- lib/build.gradle.kts | 12 +- .../bip32ed25519/ContextualApiCrypto.kt | 294 ++++++++---------- .../bip32ed25519/ContextualApiCryptoTest.kt | 35 +++ lib/src/test/resources/auth.request.json | 203 ++++++++++++ 4 files changed, 378 insertions(+), 166 deletions(-) create mode 100644 lib/src/test/resources/auth.request.json diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index f0d8b44..f43bb03 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -25,8 +25,16 @@ dependencies { // Bip39 implementation implementation("cash.z.ecc.android:kotlin-bip39:1.0.7") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + // Data validation + // https://mvnrepository.com/artifact/org.jetbrains.kotlinx + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.6.3") + // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") + // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core + implementation("com.fasterxml.jackson.core:jackson-core:2.16.1") + // Use the Kotlin JUnit 5 integration. testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/lib/src/main/kotlin/bip32ed25519/ContextualApiCrypto.kt b/lib/src/main/kotlin/bip32ed25519/ContextualApiCrypto.kt index bf6e583..2b3f9b3 100644 --- a/lib/src/main/kotlin/bip32ed25519/ContextualApiCrypto.kt +++ b/lib/src/main/kotlin/bip32ed25519/ContextualApiCrypto.kt @@ -3,6 +3,8 @@ */ package bip32ed25519 +// import kotlinx.serialization.cbor.Cbor + import com.goterl.lazysodium.LazySodiumJava import com.goterl.lazysodium.SodiumJava import com.goterl.lazysodium.utils.LibraryLoader @@ -16,9 +18,6 @@ import kotlinx.serialization.Serializable enum class KeyContext(val value: Int) { Address(0), Identity(1), - TESTVECTOR_1(2), - TESTVECTOR_2(3), - TESTVECTOR_3(4), } @Serializable data class ChannelKeys(val tx: ByteArray, val rx: ByteArray) {} @@ -30,6 +29,12 @@ enum class Encoding { NONE } +enum class GetMode { + Scalar, + PublicKey, + Raw +} + fun printer(input: ByteArray): String { var s = "(" for (i in input) { @@ -51,18 +56,6 @@ class ContextualApiCrypto(private var seed: ByteArray) { this.lazySodium = LazySodiumJava(SodiumJava(LibraryLoader.Mode.BUNDLED_ONLY)) } - fun crypto_core_ed25519_scalar_add(a: ByteArray, b: ByteArray): ByteArray { - return this.lazySodium.cryptoCoreEd25519ScalarAdd(a, b).toByteArray() - } - - fun crypto_core_ed25519_scalar_mul(a: ByteArray, b: ByteArray): ByteArray { - return this.lazySodium.cryptoCoreEd25519ScalarMul(a, b).toByteArray() - } - - fun crypto_core_ed25519_scalar_reduce(a: BigInteger): ByteArray { - return this.lazySodium.cryptoCoreEd25519ScalarReduce(a).toByteArray() - } - fun harden(num: UInt): UInt = 0x80000000.toUInt() + num fun getBIP44PathFromContext(context: KeyContext, account: UInt, keyIndex: UInt): List { @@ -133,7 +126,11 @@ class ContextualApiCrypto(private var seed: ByteArray) { * @returns * - (z, c) where z is the 64-byte child key and c is the chain code */ - fun deriveNonHardened(kl: ByteArray, cc: ByteArray, index: UInt): Pair { + internal fun deriveNonHardened( + kl: ByteArray, + cc: ByteArray, + index: UInt + ): Pair { val data = ByteBuffer.allocate(1 + 32 + 4) data.put(1 + 32, index.toByte()) @@ -168,7 +165,7 @@ class ContextualApiCrypto(private var seed: ByteArray) { * @returns * - (z, c) where z is the 64-byte child key and c is the chain code */ - fun deriveHardened( + internal fun deriveHardened( kl: ByteArray, kr: ByteArray, cc: ByteArray, @@ -210,7 +207,7 @@ class ContextualApiCrypto(private var seed: ByteArray) { * - (kL, kR, c) where kL is the left 32 bytes of the child key (the new scalar), kR is the * right 32 bytes of the child key, and c is the chain code. Total 96 bytes */ - fun deriveChildNodePrivate(extendedKey: ByteArray, index: UInt): ByteArray { + internal fun deriveChildNodePrivate(extendedKey: ByteArray, index: UInt): ByteArray { val kl = extendedKey.sliceArray(0 until 32) val kr = extendedKey.sliceArray(32 until 64) val cc = extendedKey.sliceArray(64 until 96) @@ -255,11 +252,15 @@ class ContextualApiCrypto(private var seed: ByteArray) { * - BIP44 path (m / purpose' / coin_type' / account' / change / address_index). The ' indicates * that the value is hardened * @param isPrivate - * - if true, return the private key, otherwise return the public key + * - returns full 64 bytes privatekey (first 32 bytes scalar), false returns 32 byte public key, * @returns * - The public key of 32 bytes. If isPrivate is true, returns the private key instead. */ - fun deriveKey(rootKey: ByteArray, bip44Path: List, isPrivate: Boolean = true): ByteArray { + internal fun deriveKey( + rootKey: ByteArray, + bip44Path: List, + isPrivate: Boolean + ): ByteArray { var derived = this.deriveChildNodePrivate(rootKey, bip44Path[0]) derived = this.deriveChildNodePrivate(derived, bip44Path[1]) derived = this.deriveChildNodePrivate(derived, bip44Path[2]) @@ -279,9 +280,14 @@ class ContextualApiCrypto(private var seed: ByteArray) { // 32) derived = this.deriveChildNodePrivate(derived, bip44Path[4]) - val scalar = derived.sliceArray(0 until 32) // scalar == pvtKey - return if (isPrivate) scalar - else this.lazySodium.cryptoScalarMultEd25519BaseNoclamp(scalar).toBytes() + + if (isPrivate) { + return derived + } else { + return this.lazySodium + .cryptoScalarMultEd25519BaseNoclamp(derived.sliceArray(0 until 32)) + .toBytes() + } } /** @@ -302,164 +308,124 @@ class ContextualApiCrypto(private var seed: ByteArray) { return this.deriveKey(rootKey, bip44Path, false) } - // /** - // * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 - // * - // * Edwards-Curve Digital Signature Algorithm (EdDSA) - // * - // * @param context - // * - context of the key (i.e Address, Identity) - // * @param account - // * - account number. This value will be hardened as part of BIP44 - // * @param keyIndex - // * - key index. This value will be a SOFT derivation as part of BIP44. - // * @param data - // * - data to be signed in raw bytes - // * @param metadata - // * - metadata object that describes how `data` was encoded and what schema to use to validate - // * against - // * - // * @returns - // * - signature holding R and S, totally 64 bytes - // */ - // suspend fun signData( - // context: KeyContext, - // account: Int, - // keyIndex: Int, - // data: ByteArray, - // metadata: SignMetadata - // ): Any { - // // validate data - - // // TODO: Re add this data validation logic - // // val result = validateData(data, metadata) - - // // if (result is Error) { // decoding errors - // // throw result - // // } - - // // if (!result) { // failed schema validation - // // throw ERROR_BAD_DATA - // // } - - // // Assuming ready is a CompletableFuture that ensures libsodium is ready - // // ready.join() - - // val rootKey: ByteArray = fromSeed(this.seed) - // val bip44Path: List = getBIP44PathFromContext(context, account, keyIndex) - // val raw: ByteArray = deriveKey(rootKey, bip44Path, true) - - // val scalar = raw.sliceArray(0 until 32) - // val c = raw.sliceArray(32 until 64) - - // // \(1): pubKey = scalar * G (base point, no clamp) - // val publicKey = - // this@ContextualApiCrypto.lazySodium.cryptoScalarMultEd25519BaseNoclamp(scalar).toBytes() - - // // \(2): h = hash(c + msg) mod q - // val hash = BigInteger(1, MessageDigest.getInstance("SHA-512").digest(c + data)) - // val q = - // BigInteger( - // - // "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED", - // 16 - // ) - // val rBigInt = hash.mod(q) - - // // fill 32 bytes of r - // // convert to ByteArray - // val r = ByteArray(32) - // val rBString = rBigInt.toString(16).padStart(64, '0') // convert to hex - - // for (i in r.indices) { - // r[i] = DatatypeConverter.parseHexBinary(rBString.substring(i * 2, i * 2 + - // 2))[0] - // } - - // // \(4): R = r * G (base point, no clamp) - // val R = this@ContextualApiCrypto.cryptoScalarMultEd25519BaseNoclamp(r).toBytes() - - // var h = MessageDigest.getInstance("SHA-512").digest(R + publicKey + data) - // h = this@ContextualApiCrypto.crypto_core_ed25519_scalar_reduce(BigInteger(1, h)) - - // // \(5): S = (r + h * k) mod q - // val S = - // this@ContextualApiCrypto.crypto_core_ed25519_scalar_add( - // r, - // this@ContextualApiCrypto.crypto_core_ed25519_scalar_mul(h, - // scalar) - // ) - - // R + S - // } - /** - * Impelemntation how to validate data with encoding and schema, using base64 as an example + * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 * - * @param message + * Edwards-Curve Digital Signature Algorithm (EdDSA) + * + * @param context + * - context of the key (i.e Address, Identity) + * @param account + * - account number. This value will be hardened as part of BIP44 + * @param keyIndex + * - key index. This value will be a SOFT derivation as part of BIP44. + * @param data + * - data to be signed in raw bytes * @param metadata + * - metadata object that describes how `data` was encoded and what schema to use to validate + * against + * * @returns + * - signature holding R and S, totally 64 bytes */ - private fun validateData(message: ByteArray, metadata: SignMetadata): Boolean { - // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program - // These prefixes are reserved for the protocol + fun signData( + context: KeyContext, + account: UInt, + keyIndex: UInt, + data: ByteArray, + metadata: Any, // SignMetadata + ): ByteArray { + // validate data - // if (this.hasAlgorandTags(message)) { - // throw IllegalArgumentException(ERROR_TAGS_FOUND) - // } - // - // val decoded: ByteArray - // when (metadata.encoding) { - // Encoding.BASE64 -> - // decoded = Base64.getDecoder().decode(message.toString(Charsets.UTF_8)) - // Encoding.MSGPACK -> - // decoded = - // MessagePack.newDefaultUnpacker(message) - // .unpackValue() - // .asBinaryValue() - // .asByteArray() - // Encoding.NONE -> decoded = message - // else -> throw IllegalArgumentException("Invalid encoding") - // } + // TODO: Re add this data validation logic + // val result = validateData(data, metadata) - // // Check after decoding too - // // Some one might try to encode a regular transaction with the protocol reserved prefixes - // if (this.hasAlgorandTags(decoded)) { - // throw IllegalArgumentException(ERROR_TAGS_FOUND) + // if (result is Error) { // decoding errors + // throw result // } - // // validate with schema - // val mapper = jacksonObjectMapper() - // val jsonNode = mapper.readValue(decoded.toString(Charsets.UTF_8)) - // val schemaNode = mapper.convertValue(metadata.schema, JsonNode::class.java) - - // val factory = JsonSchemaFactory.byDefault() - // val validator: JsonValidator = factory.validator - // val report: ProcessingReport = validator.validate(schemaNode, jsonNode) - - // if (!report.isSuccess) println(report) + // if (!result) { // failed schema validation + // throw ERROR_BAD_DATA + // } - // return report.isSuccess - return true + val rootKey: ByteArray = fromSeed(this.seed) + val bip44Path: List = getBIP44PathFromContext(context, account, keyIndex) + val raw: ByteArray = deriveKey(rootKey, bip44Path, true) + + val scalar = raw.sliceArray(0 until 32) + val c = raw.sliceArray(32 until 64) + + // \(1): pubKey = scalar * G (base point, no clamp) + val publicKey = this.lazySodium.cryptoScalarMultEd25519BaseNoclamp(scalar).toBytes() + + // \(2): r = hash(c + msg) mod q [LE] + val rHash = MessageDigest.getInstance("SHA-512").digest(c + data).reversedArray() + val r = this.lazySodium.cryptoCoreEd25519ScalarReduce(rHash).toByteArray().reversedArray() + + // \(4): R = r * G (base point, no clamp) + val R = this.lazySodium.cryptoScalarMultEd25519BaseNoclamp(r).toBytes() + + var h = MessageDigest.getInstance("SHA-512").digest(R + publicKey + data) + h = this.lazySodium.cryptoCoreEd25519ScalarReduce(h).toByteArray().reversedArray() + + // \(5): S = (r + h * k) mod q + val S = + this.lazySodium + .cryptoCoreEd25519ScalarAdd( + r, + this.lazySodium + .cryptoCoreEd25519ScalarMul(h, scalar) + .toByteArray() + .reversedArray() + ) + .toByteArray() + .reversedArray() + return R + S } /** - * Detect if the message has Algorand protocol specific tags + * Impelemntation how to validate data with encoding and schema, using base64 as an example * * @param message - * - raw bytes of the message + * @param metadata * @returns - * - true if message has Algorand protocol specific tags, false otherwise */ - private fun hasAlgorandTags(message: ByteArray): Boolean { - // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program - val tx = String(message.sliceArray(0..1), Charsets.US_ASCII) - val mx = String(message.sliceArray(0..1), Charsets.US_ASCII) - val progData = String(message.sliceArray(0..7), Charsets.US_ASCII) - val program = String(message.sliceArray(0..6), Charsets.US_ASCII) - - return tx == "TX" || mx == "MX" || progData == "progData" || program == "Program" - } + // fun validateData(message: ByteArray, metadata: SignMetadata): Boolean { + // // Check for Algorand tags + // if (hasAlgorandTags(message)) { + // return false // Assuming ERROR_TAGS_FOUND maps to false + // } + + // val decoded: ByteArray = + // when (metadata.encoding) { + // Encoding.BASE64 -> Base64.getDecoder().decode(message) + // Encoding.MSGPACK -> Cbor.decodeFromByteArray(message) + // Encoding.NONE -> message + // else -> throw IllegalArgumentException("Invalid encoding") + // } + + // // Check after decoding too + // if (hasAlgorandTags(decoded)) { + // return false + // } + + // // Validate with schema + // val ajv = JsonSchemaFactory.byDefault() + // val jsonNode = jacksonObjectMapper().readValue(decoded) + // val validationReport = ajv.getJsonSchema(metadata.schema).validate(jsonNode) + + // if (!validationReport.isSuccess) { + // println(validationReport) + // } + + // return validationReport.isSuccess + // } + + // fun hasAlgorandTags(message: ByteArray): Boolean { + // val prefixes = listOf("TX", "MX", "progData", "Program") + // val messageString = String(message) + // return prefixes.any { messageString.startsWith(it) } + // } /** * Wrapper around libsodium basic signature verification diff --git a/lib/src/test/kotlin/bip32ed25519/ContextualApiCryptoTest.kt b/lib/src/test/kotlin/bip32ed25519/ContextualApiCryptoTest.kt index 9cd0c94..109d3cd 100644 --- a/lib/src/test/kotlin/bip32ed25519/ContextualApiCryptoTest.kt +++ b/lib/src/test/kotlin/bip32ed25519/ContextualApiCryptoTest.kt @@ -287,6 +287,7 @@ class ContextualApiCryptoTest { @Test fun fromSeedBip39Test() { + // seedArray: // 58,255,45,180,22,184,149,236,60,249,164,248,209,233,112,188,152,25,146,14,123,244,74,94,53,4,119,175,14,245,87,177,81,27,9,134,222,191,120,221,56,199,197,32,205,68,255,124,114,49,97,143,149,142,33,239,2,80,115,58,140,25,21,234 ////////// @@ -453,4 +454,38 @@ class ContextualApiCryptoTest { } } } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + internal class SignTypedDataTests { + private lateinit var c: ContextualApiCrypto + + @BeforeAll + fun setup() { + c = ContextualApiCrypto(seedArray) + } + + @Test + fun simpleSignDataTest() { + val data = "Hello World".toByteArray() + + val pubKey = c.keyGen(KeyContext.Address, 0u, 0u) + val signature = c.signData(KeyContext.Address, 0u, 0u, data, "") + val isValid = c.verifyWithPublicKey(signature, data, pubKey) + assert(isValid) { "signature is not valid" } + + val pubKey2 = c.keyGen(KeyContext.Address, 0u, 1u) + assert(!c.verifyWithPublicKey(signature, data, pubKey2)) { + "signature is unexpectedly valid" + } + } + + // @Test + // fun signAuthChallengeTest() { + // val challenge = Random.nextBytes(ByteArray(32)) + // val path = Paths.get(ClassLoader.getSystemResource("auth.request.json").toURI()) + // val metadata = SignMetadata(Encoding.BASE64, authSchema) + + // val encoded = Base64.getEncoder().encodeToString(Files.readAllBytes(path)) + // } + } } diff --git a/lib/src/test/resources/auth.request.json b/lib/src/test/resources/auth.request.json new file mode 100644 index 0000000..9097118 --- /dev/null +++ b/lib/src/test/resources/auth.request.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://arc52/schemas/auth.request.json", + "title": "Payment Transaction", + "type": "object", + "additionalProperties": false, + "properties": { + "0": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "1": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "2": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "3": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "4": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "5": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "6": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "7": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "8": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "9": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "10": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "11": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "12": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "13": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "14": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "15": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "16": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "17": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "18": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "19": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "20": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "21": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "22": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "23": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "24": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "25": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "26": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "27": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "28": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "29": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "30": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "31": { + "type": "integer", + "minimum": 0, + "maximum": 255 + } + }, + "required": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31" + ] +} \ No newline at end of file