Skip to content

Commit

Permalink
Merge pull request #1272 from WalletConnect/sign_2.5_recaps
Browse files Browse the repository at this point in the history
CAIP-222/ReCaps
  • Loading branch information
jakubuid authored Feb 2, 2024
2 parents ad4e440 + d04eece commit 92b3c1b
Show file tree
Hide file tree
Showing 65 changed files with 922 additions and 352 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ open class CoreSignParams : ClientParams {
data class SessionAuthenticateApproveParams(
@Json(name = "participant")
val responder: Participant,
@Json(name = "caip222Response")
val caip222Response: List<Cacao>,
@Json(name = "cacaos")
val cacaos: List<Cacao>,
) : CoreSignParams()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import androidx.annotation.Keep
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.walletconnect.android.cacao.SignatureInterface
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.ACTION_DELIMITER
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.ACTION_POSITION
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.ACTION_TYPE_POSITION
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.ATT_KEY
import com.walletconnect.android.internal.common.signing.cacao.Cacao.Payload.Companion.RECAPS_PREFIX
import com.walletconnect.android.internal.common.signing.signature.Signature
import org.bouncycastle.util.encoders.Base64
import org.json.JSONObject

@JsonClass(generateAdapter = true)
data class Cacao(
Expand Down Expand Up @@ -57,19 +64,30 @@ data class Cacao(
@Json(name = "resources")
val resources: List<String>?,
) {
val actionsString get() = getActionsString(Issuer(iss))
val methods get() = getActions(Issuer(iss))

companion object {
const val CURRENT_VERSION = "1"
const val ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
const val RECAPS_PREFIX = "urn:recap:"
const val ATT_KEY = "att"
const val ACTION_TYPE_POSITION = 0
const val ACTION_POSITION = 1
const val ACTION_DELIMITER = "/"
}
}
}

@JvmSynthetic
internal fun Cacao.Signature.toSignature(): Signature = Signature.fromString(s)

fun Cacao.Payload.toCAIP122Message(chainName: String = "Ethereum"): String {
fun Cacao.Payload.toCAIP222Message(chainName: String = "Ethereum"): String {
var message = "$domain wants you to sign in with your $chainName account:\n${Issuer(iss).address}\n\n"
if (statement != null) message += "$statement\n"
if (statement != null) message += "$statement"
if (resources?.find { r -> r.startsWith(RECAPS_PREFIX) } != null) message += " I further authorize the stated URI to perform the following actions on my behalf: (1) $actionsString for '${
Issuer(iss).namespace
}'\n"
message += "\nURI: $aud\nVersion: $version\nChain ID: ${Issuer(iss).chainIdReference}\nNonce: $nonce\nIssued At: $iat"
if (exp != null) message += "\nExpiration Time: $exp"
if (nbf != null) message += "\nNot Before: $nbf"
Expand All @@ -78,5 +96,29 @@ fun Cacao.Payload.toCAIP122Message(chainName: String = "Ethereum"): String {
message += "\nResources:"
resources.forEach { resource -> message += "\n- $resource" }
}

return message
}

private fun Cacao.Payload.getActionsString(issuer: Issuer): String {
return decodeReCaps(issuer).entries.joinToString(", ") { "'${it.key}': " + it.value.joinToString(", ") { value -> "'$value'" } }
}

private fun Cacao.Payload.getActions(issuer: Issuer): List<String> {
return decodeReCaps(issuer).values.flatten()
}

private fun Cacao.Payload.decodeReCaps(issuer: Issuer): MutableMap<String, MutableList<String>> {
val encodedReCaps = resources?.find { resource -> resource.startsWith(RECAPS_PREFIX) }?.removePrefix(RECAPS_PREFIX) ?: throw Exception()
val reCaps = Base64.decode(encodedReCaps).toString(Charsets.UTF_8)
val requests = (JSONObject(reCaps).get(ATT_KEY) as JSONObject).getJSONArray(issuer.namespace)
val actions: MutableMap<String, MutableList<String>> = mutableMapOf()

for (i in 0 until requests.length()) {
val actionString = requests.getJSONObject(i).keys().next().toString()
val actionType = actionString.split(ACTION_DELIMITER)[ACTION_TYPE_POSITION]
val action = actionString.split(ACTION_DELIMITER)[ACTION_POSITION]
actions[actionType]?.add(action) ?: actions.put(actionType, mutableListOf(action))
}
return actions
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class CacaoVerifier(private val projectId: ProjectId) {
fun verify(cacao: Cacao): Boolean = when (cacao.signature.t) {

SignatureType.EIP191.header, SignatureType.EIP1271.header -> {
val plainMessage = cacao.payload.toCAIP122Message()
val hexMessage = Numeric.toHexString(cacao.payload.toCAIP122Message().toByteArray())
val plainMessage = cacao.payload.toCAIP222Message()
val hexMessage = Numeric.toHexString(cacao.payload.toCAIP222Message().toByteArray())
val address = Issuer(cacao.payload.iss).address

if (cacao.signature.toSignature().verify(plainMessage, address, cacao.signature.t, projectId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ data class Issuer(val value: String) {
get() = value.split(ISS_DELIMITER)[ISS_POSITION_OF_REFERENCE]
val address: String
get() = value.split(ISS_DELIMITER)[ISS_POSITION_OF_ADDRESS]
val namespace
get() = value.split(ISS_DELIMITER)[ISS_POSITION_OF_NAMESPACE]
val accountId
get() = "$chainId:$address"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class JsonRpcHistory(
return if (record != null && record.response == null) record else null

Check warning on line 67 in core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/rpc/JsonRpcHistory.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for Android

Constant conditions

Condition 'record != null \&\& record.response == null' is always false

Check warning on line 67 in core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/rpc/JsonRpcHistory.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for Android

Constant conditions

Condition 'record != null' is always true

Check warning on line 67 in core/android/src/main/kotlin/com/walletconnect/android/internal/common/storage/rpc/JsonRpcHistory.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for Android

Constant conditions

Condition 'record.response == null' is always false when reached
}

fun getRecordById(id: Long): JsonRpcHistoryRecord? {
return jsonRpcHistoryQueries.getJsonRpcHistoryRecord(id, mapper = ::toRecord).executeAsOneOrNull()
}

private fun toRecord(requestId: Long, topic: String, method: String, body: String, response: String?): JsonRpcHistoryRecord =
JsonRpcHistoryRecord(requestId, topic, method, body, response)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.walletconnect.android.internal.common.signing.cacao.Cacao
import com.walletconnect.android.internal.common.signing.cacao.CacaoType
import com.walletconnect.android.internal.common.signing.cacao.CacaoVerifier
import com.walletconnect.android.internal.common.signing.cacao.Issuer
import com.walletconnect.android.internal.common.signing.cacao.toCAIP122Message
import com.walletconnect.android.internal.common.signing.cacao.toCAIP222Message
import com.walletconnect.android.internal.common.storage.identity.IdentitiesStorageRepository
import com.walletconnect.android.internal.utils.getIdentityTag
import com.walletconnect.android.keyserver.domain.use_case.RegisterIdentityUseCase
Expand Down Expand Up @@ -154,7 +154,7 @@ class IdentitiesInteractor(

private fun generateAndSignCacao(accountId: AccountId, identityKey: PublicKey, statement: String, domain: String, resources: List<String>, onSign: (String) -> Cacao.Signature?): Result<Cacao> {
val payload = generatePayload(accountId, identityKey, statement, domain, resources).getOrThrow()
val message = payload.toCAIP122Message()
val message = payload.toCAIP222Message()
val signature = onSign(message) ?: throw UserRejectedSigning()
return Result.success(Cacao(CacaoType.EIP4361.toHeader(), payload, signature))
}
Expand Down
4 changes: 2 additions & 2 deletions core/android/src/test/java/CacaoTestJvmTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void jvmTestHexStringSigning() {
);

String chainName = "Ethereum";
String message = CacaoKt.toCAIP122Message(payload, chainName);
String message = CacaoKt.toCAIP222Message(payload, chainName);
SignatureTest signature = CacaoSignerUtil.signHex(SignatureTest.class, Numeric.toHexString(message.getBytes(StandardCharsets.UTF_8)), privateKey, SignatureType.EIP191);
Cacao.Signature cacaoSig = new Cacao.Signature(signature.getT(), signature.getS(), signature.getM());
Cacao cacao = new Cacao(CacaoType.EIP4361.toHeader(), payload, cacaoSig);
Expand Down Expand Up @@ -72,7 +72,7 @@ public void jvmTestPlainStringSigning() {
);

String chainName = "Ethereum";
String message = CacaoKt.toCAIP122Message(payload, chainName);
String message = CacaoKt.toCAIP222Message(payload, chainName);
SignatureTest signature = CacaoSignerUtil.sign(SignatureTest.class, message, privateKey, SignatureType.EIP191);
Cacao.Signature cacaoSig = new Cacao.Signature(signature.getT(), signature.getS(), signature.getM());
Cacao cacao = new Cacao(CacaoType.EIP4361.toHeader(), payload, cacaoSig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.walletconnect.android.internal.common.model.ProjectId
import com.walletconnect.android.internal.common.signing.cacao.Cacao
import com.walletconnect.android.internal.common.signing.cacao.CacaoType
import com.walletconnect.android.internal.common.signing.cacao.CacaoVerifier
import com.walletconnect.android.internal.common.signing.cacao.toCAIP122Message
import com.walletconnect.android.internal.common.signing.cacao.toCAIP222Message
import com.walletconnect.android.utils.cacao.CacaoSignerInterface
import com.walletconnect.android.utils.cacao.sign
import com.walletconnect.android.utils.cacao.signHex
Expand All @@ -31,27 +31,31 @@ internal class CacaoTest {
exp = null,
statement = "I accept the ServiceOrg Terms of Service: https://service.invalid/tos",
requestId = null,
resources = listOf("ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", "https://example.com/my-web2-claim.json")
resources = listOf(
"ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/",
"https://example.com/my-web2-claim.json",
"urn:recap:eyJhdHQiOnsiZWlwMTU1IjpbeyJyZXF1ZXN0L2V0aF9zaWduVHlwZWREYXRhX3Y0IjpbXX0seyJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOltdfV19fQ=="
)
)

private val privateKey = "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a".hexToBytes()

@Test
fun signAndVerifyWithEIP191Test() {
print(payload.toCAIP122Message(chainName))
val message = payload.toCAIP122Message(chainName)
print(payload.toCAIP222Message(chainName))
val message = payload.toCAIP222Message(chainName)
val signature: Cacao.Signature = cacaoSigner.sign(message, privateKey, SignatureType.EIP191)
val cacao = Cacao(CacaoType.EIP4361.toHeader(), payload, signature)
val cacao = Cacao(CacaoType.CAIP222.toHeader(), payload, signature)
val result: Boolean = cacaoVerifier.verify(cacao)
Assert.assertTrue(result)
}

@Test
fun signHexAndVerifyWithEIP191Test() {
print(payload.toCAIP122Message(chainName))
val message = payload.toCAIP122Message(chainName)
println(payload.toCAIP222Message(chainName))
val message = payload.toCAIP222Message(chainName)
val signature: Cacao.Signature = cacaoSigner.signHex(Numeric.toHexString(message.toByteArray()), privateKey, SignatureType.EIP191)
val cacao = Cacao(CacaoType.EIP4361.toHeader(), payload, signature)
val cacao = Cacao(CacaoType.CAIP222.toHeader(), payload, signature)
val result: Boolean = cacaoVerifier.verify(cacao)
assert(result)
}
Expand Down Expand Up @@ -100,7 +104,7 @@ internal class CacaoTest {
)

val signatureString = "0xdeaddeaddead4095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c"
val signature: Cacao.Signature = Cacao.Signature(SignatureType.EIP1271.header, signatureString, payload.toCAIP122Message())
val signature: Cacao.Signature = Cacao.Signature(SignatureType.EIP1271.header, signatureString, payload.toCAIP222Message())
val cacao = Cacao(CacaoType.EIP4361.toHeader(), payload, signature)
val result: Boolean = cacaoVerifier.verify(cacao)
Assert.assertFalse(result)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.walletconnect.android.internal.common.cacao

import com.walletconnect.android.Core
import com.walletconnect.android.internal.common.signing.cacao.Cacao
import com.walletconnect.android.internal.common.signing.cacao.toCAIP122Message
import com.walletconnect.android.internal.common.signing.cacao.toCAIP222Message
import junit.framework.TestCase.assertEquals
import org.junit.Test

internal class MapperTest {
private val iss = "did:pkh:eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688"
private val chainName = "Ethereum"
private val dummyPairing = Core.Model.Pairing("", 0L, null, "", null, "", true, "")

@Test
fun `Payload required fields formatting`() {
Expand Down Expand Up @@ -37,7 +35,7 @@ internal class MapperTest {
"Nonce: 32891756\n" +
"Issued At: 2021-09-30T16:25:24Z"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand All @@ -51,14 +49,19 @@ internal class MapperTest {
iat = "2021-09-30T16:25:24Z",
nbf = null,
exp = null,
statement = null,
statement = "Statement",
requestId = null,
resources = listOf("ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", "https://example.com/my-web2-claim.json")
resources = listOf(
"ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/",
"https://example.com/my-web2-claim.json",
"urn:recap:eyJhdHQiOnsiZWlwMTU1IjpbeyJyZXF1ZXN0L2V0aF9zaWduVHlwZWREYXRhX3Y0IjpbXX0seyJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOltdfV19fQ=="
)
)

val message = "service.invalid wants you to sign in with your Ethereum account:\n" +
"0x15bca56b6e2728aec2532df9d436bd1600e86688\n" +
"\n" +
"Statement I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_signTypedData_v4', 'personal_sign' for 'eip155'\n" +
"\n" +
"URI: https://service.invalid/login\n" +
"Version: 1\n" +
Expand All @@ -67,9 +70,10 @@ internal class MapperTest {
"Issued At: 2021-09-30T16:25:24Z\n" +
"Resources:\n" +
"- ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/\n" +
"- https://example.com/my-web2-claim.json"
"- https://example.com/my-web2-claim.json\n" +
"- urn:recap:eyJhdHQiOnsiZWlwMTU1IjpbeyJyZXF1ZXN0L2V0aF9zaWduVHlwZWREYXRhX3Y0IjpbXX0seyJyZXF1ZXN0L3BlcnNvbmFsX3NpZ24iOltdfV19fQ=="

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand Down Expand Up @@ -99,7 +103,7 @@ internal class MapperTest {
"Issued At: 2021-09-30T16:25:24Z\n" +
"Request ID: someRequestId"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand Down Expand Up @@ -129,7 +133,7 @@ internal class MapperTest {
"Nonce: 32891756\n" +
"Issued At: 2021-09-30T16:25:24Z"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand Down Expand Up @@ -159,7 +163,7 @@ internal class MapperTest {
"Issued At: 2021-09-30T16:25:24Z\n" +
"Expiration Time: 2021-09-31T16:25:24Z"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand Down Expand Up @@ -189,7 +193,7 @@ internal class MapperTest {
"Issued At: 2021-09-30T16:25:24Z\n" +
"Not Before: 2021-09-31T16:25:24Z"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}

@Test
Expand Down Expand Up @@ -225,6 +229,6 @@ internal class MapperTest {
"- ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/\n" +
"- https://example.com/my-web2-claim.json"

assertEquals(message, payload.toCAIP122Message(chainName))
assertEquals(message, payload.toCAIP222Message(chainName))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.walletconnect.android.cacao.signature.SignatureType
import com.walletconnect.android.internal.common.signing.cacao.Cacao
import com.walletconnect.android.internal.common.signing.cacao.toCAIP122Message
import com.walletconnect.android.internal.common.signing.cacao.toCAIP222Message
import com.walletconnect.android.internal.common.signing.cacao.toSignature
import com.walletconnect.android.internal.common.signing.eip191.EIP191Signer
import com.walletconnect.android.internal.common.signing.eip191.EIP191Verifier
Expand Down Expand Up @@ -116,7 +116,7 @@ internal class EIP191SignerTest {
val jsonAdapter: JsonAdapter<Cacao> = moshi.adapter(Cacao::class.java)
val cacao = jsonAdapter.fromJson(cacaoAsJson)
println(cacao)
val message = cacao!!.payload.toCAIP122Message()
val message = cacao!!.payload.toCAIP222Message()
val signature = cacaoSigner.sign(message, privateKey, SignatureType.EIP191)
println(signature)
println("Message:")
Expand Down
Loading

0 comments on commit 92b3c1b

Please sign in to comment.