From de5720897a08e2c0ce4875fca4890e00d5c16c68 Mon Sep 17 00:00:00 2001 From: Vyacheslav Shebanov Date: Fri, 17 Jun 2022 05:08:18 +0300 Subject: [PATCH] problem: cannot verify signature in browser solution: support NIST P-256 curve which is supported by browser's webcrypto --- docs/reference-configuration.adoc | 2 +- .../dshackle/config/SignatureConfig.kt | 14 ++++++- .../{Secp256KSigner.kt => EcdsaSigner.kt} | 2 +- .../signature/ResponseSignerFactory.kt | 20 ++++++---- ...gnerSpec.groovy => EcdsaSignerSpec.groovy} | 37 ++++++++++++++----- .../main/groovy/testing/CommonHandlers.groovy | 12 ++++++ .../main/groovy/testing/SimpleUpstream.groovy | 2 +- .../src/main/resources/chain-id.json | 5 +++ 8 files changed, 73 insertions(+), 21 deletions(-) rename src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/{Secp256KSigner.kt => EcdsaSigner.kt} (99%) rename src/test/groovy/io/emeraldpay/dshackle/upstream/signature/{Secp256KSignerSpec.groovy => EcdsaSignerSpec.groovy} (79%) create mode 100644 testing/simple-upstream/src/main/resources/chain-id.json diff --git a/docs/reference-configuration.adoc b/docs/reference-configuration.adoc index b90631d8..4410c48a 100644 --- a/docs/reference-configuration.adoc +++ b/docs/reference-configuration.adoc @@ -578,7 +578,7 @@ signed-response: | `algorithm` | `SECP256K1` -| SECP256K1 only possible at this moment +| `SECP256K1` or `NIST-P256` | `private-key` | diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/SignatureConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/SignatureConfig.kt index 29b94f64..6b1d8e85 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/SignatureConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/SignatureConfig.kt @@ -5,13 +5,25 @@ import java.util.Locale class SignatureConfig { enum class Algorithm { - SECP256K1 + SECP256K1, + NIST_P256; + + fun getCurveName(): String { + return if (this == SECP256K1) { + "secp256k1" + } else if (this == NIST_P256) { + "secp256r1" + } else { + throw IllegalStateException() + } + } } companion object { fun algorithmOfString(algo: String): Algorithm { val algorithm = when (algo.uppercase(Locale.getDefault())) { "SECP256K1" -> Algorithm.SECP256K1 + "NIST_P256", "NIST-P256", "NISTP256", "SECP256R1" -> Algorithm.NIST_P256 else -> throw IllegalArgumentException("Unknown algorithm or not allowed") } return algorithm diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/Secp256KSigner.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/EcdsaSigner.kt similarity index 99% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/Secp256KSigner.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/EcdsaSigner.kt index 262a4ef2..10039c13 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/Secp256KSigner.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/EcdsaSigner.kt @@ -6,7 +6,7 @@ import java.security.MessageDigest import java.security.Signature import java.security.interfaces.ECPrivateKey -class Secp256KSigner( +class EcdsaSigner( private val privateKey: ECPrivateKey, val keyId: Long, ) : ResponseSigner { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/ResponseSignerFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/ResponseSignerFactory.kt index 19b318fd..980bcda3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/ResponseSignerFactory.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/ResponseSignerFactory.kt @@ -37,28 +37,32 @@ open class ResponseSignerFactory( private fun readKey(algorithm: SignatureConfig.Algorithm, pem: PemObject): Pair { val keyFactory = KeyFactory.getInstance("EC") val key = when (algorithm) { - SignatureConfig.Algorithm.SECP256K1 -> { + SignatureConfig.Algorithm.SECP256K1, SignatureConfig.Algorithm.NIST_P256 -> { val keySpec = PKCS8EncodedKeySpec(pem.content) keyFactory.generatePrivate(keySpec) } } if (key !is ECPrivateKey) { - throw IllegalStateException("Only ECDSA SECP256K1 keys are allowed") + throw IllegalStateException("Only EC keys are allowed") } - if (key.params.toString() != "secp256k1 (1.3.132.0.10)") { - throw IllegalStateException("Only SECP256K1 are allowed for signing a response") + if (algorithm == SignatureConfig.Algorithm.SECP256K1 && key.params.toString().indexOf(SignatureConfig.Algorithm.SECP256K1.getCurveName()) < 0) { + throw IllegalStateException("Key is not SECP256K1, generate SECP256K1 or use another algorithm") } - val publicKey = extractPublicKey(keyFactory, key) + if (algorithm == SignatureConfig.Algorithm.NIST_P256 && key.params.toString().indexOf(SignatureConfig.Algorithm.NIST_P256.getCurveName()) < 0) { + throw IllegalStateException("Key is not NIST P256, generate NIST P256 or use another algorithm") + } + + val publicKey = extractPublicKey(keyFactory, key, algorithm) val id = getPublicKeyId(publicKey) return Pair(key, id) } - fun extractPublicKey(keyFactory: KeyFactory, privateKey: ECPrivateKey): PublicKey { - val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1") + fun extractPublicKey(keyFactory: KeyFactory, privateKey: ECPrivateKey, algorithm: SignatureConfig.Algorithm): PublicKey { + val ecSpec = ECNamedCurveTable.getParameterSpec(algorithm.getCurveName()) val q: ECPoint = ecSpec.g.multiply(privateKey.s) return keyFactory.generatePublic(ECPublicKeySpec(q, ecSpec)) } @@ -79,7 +83,7 @@ open class ResponseSignerFactory( return NoSigner() } val key = readKey(config.algorithm, config.privateKey!!) - return Secp256KSigner(key.first, key.second) + return EcdsaSigner(key.first, key.second) } override fun getObjectType(): Class<*>? { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/Secp256KSignerSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/EcdsaSignerSpec.groovy similarity index 79% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/signature/Secp256KSignerSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/signature/EcdsaSignerSpec.groovy index 7a1762ee..7368290d 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/Secp256KSignerSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/EcdsaSignerSpec.groovy @@ -1,8 +1,6 @@ package io.emeraldpay.dshackle.upstream.signature import io.emeraldpay.dshackle.config.SignatureConfig -import io.emeraldpay.dshackle.config.SignatureConfigReader -import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.Upstream import org.apache.commons.codec.binary.Hex import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -13,14 +11,13 @@ import spock.lang.Specification import java.security.KeyFactory import java.security.KeyPairGenerator import java.security.MessageDigest -import java.security.SecureRandom import java.security.Security import java.security.Signature import java.security.interfaces.ECPrivateKey import java.security.spec.ECGenParameterSpec import java.security.spec.PKCS8EncodedKeySpec -class Secp256KSignerSpec extends Specification { +class EcdsaSignerSpec extends Specification { def setupSpec() { Security.addProvider(new BouncyCastleProvider()) @@ -48,12 +45,34 @@ class Secp256KSignerSpec extends Specification { file.delete() } + def "Reads private key NIST P256"() { + setup: + def file = File.createTempFile("test", ".pem") + def keygen = KeyPairGenerator.getInstance("EC") + keygen.initialize(new ECGenParameterSpec("secp256r1")) + def key = keygen.generateKeyPair() + def keyBuilder = new PKCS8EncodedKeySpec(key.getPrivate().getEncoded()) + def writer = new PemWriter(new FileWriter(file.path)) + writer.writeObject(new PemObject("PRIVATE KEY", keyBuilder.getEncoded())) + writer.close() + + when: + def signer = new ResponseSignerFactory(new SignatureConfig()) + def act = signer.readKey(SignatureConfig.Algorithm.NIST_P256, file.absolutePath).first + + then: + act == key.getPrivate() + + cleanup: + file.delete() + } + def "Id is a hash of x509 public key"() { setup: def conf = new SignatureConfig() conf.enabled = true conf.privateKey = "testing/dshackle/test_key" - def signer = new ResponseSignerFactory(conf).getObject() as Secp256KSigner + def signer = new ResponseSignerFactory(conf).getObject() as EcdsaSigner // To verify the test, check the hash of test key above: // @@ -73,7 +92,7 @@ class Secp256KSignerSpec extends Specification { def up = Mock(Upstream) { _ * getId() >> "infura" } - def signer = new Secp256KSigner(Stub(ECPrivateKey), 100L) + def signer = new EcdsaSigner(Stub(ECPrivateKey), 100L) when: def act = signer.wrapMessage(10, "test".bytes, up) @@ -96,7 +115,7 @@ class Secp256KSignerSpec extends Specification { verifier.initVerify(pair.getPublic()) verifier.update("DSHACKLESIG/10/infura/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08".getBytes()) - def signer = new Secp256KSigner((pair.getPrivate() as ECPrivateKey), 100L) + def signer = new EcdsaSigner((pair.getPrivate() as ECPrivateKey), 100L) when: def sig = signer.sign(10, result, up) @@ -121,12 +140,12 @@ class Secp256KSignerSpec extends Specification { def factory = new ResponseSignerFactory(conf) def sk = factory.readKey(conf.algorithm, conf.privateKey).first - def pk = factory.extractPublicKey(KeyFactory.getInstance("EC"), sk) + def pk = factory.extractPublicKey(KeyFactory.getInstance("EC"), sk, SignatureConfig.Algorithm.SECP256K1) def verifier = Signature.getInstance("SHA256withECDSA") verifier.initVerify(pk) verifier.update("DSHACKLESIG/10/infura/${Hex.encodeHexString(sha256.digest(result))}".getBytes()) - def signer = factory.getObject() as Secp256KSigner + def signer = factory.getObject() as EcdsaSigner when: def sig = signer.sign(10, result, up) diff --git a/testing/simple-upstream/src/main/groovy/testing/CommonHandlers.groovy b/testing/simple-upstream/src/main/groovy/testing/CommonHandlers.groovy index 1c5611c6..cdfdec03 100644 --- a/testing/simple-upstream/src/main/groovy/testing/CommonHandlers.groovy +++ b/testing/simple-upstream/src/main/groovy/testing/CommonHandlers.groovy @@ -1,13 +1,25 @@ package testing +import com.fasterxml.jackson.databind.ObjectMapper + class CommonHandlers implements CallHandler { + ObjectMapper objectMapper + ResourceResponse resourceResponse + + CommonHandlers(ObjectMapper objectMapper) { + this.objectMapper = objectMapper + this.resourceResponse = new ResourceResponse(objectMapper) + } @Override Result handle(String method, List params) { if (method == "eth_syncing") { return Result.ok(false) } + if (method == "eth_chainId") { + return resourceResponse.respondWith("chain-id.json") + } return null } } diff --git a/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy b/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy index 2c20b9be..b1af5642 100644 --- a/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy +++ b/testing/simple-upstream/src/main/groovy/testing/SimpleUpstream.groovy @@ -17,7 +17,7 @@ class SimpleUpstream { internalHandler = new InternalHandler() handlers << new TestcaseHandler(objectMapper) - handlers << new CommonHandlers() + handlers << new CommonHandlers(objectMapper) handlers << new BlocksHandler(objectMapper) handlers << new PingPongHandler() handlers << internalHandler diff --git a/testing/simple-upstream/src/main/resources/chain-id.json b/testing/simple-upstream/src/main/resources/chain-id.json new file mode 100644 index 00000000..c318e9d3 --- /dev/null +++ b/testing/simple-upstream/src/main/resources/chain-id.json @@ -0,0 +1,5 @@ +{ + "id": 83, + "jsonrpc": "2.0", + "result": "0x3d" +} \ No newline at end of file