diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/RsaSigner.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/RsaSigner.kt index ee0e8f04..f5439666 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/RsaSigner.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/RsaSigner.kt @@ -29,13 +29,18 @@ class RsaSigner( /** * Wrapping format: `"DSHACKLESIG/" || str(nonce) || "/" || source || "/" || hex(sha256(msg))` + * + * `nonce` carries an unsigned uint64 bit pattern (proto3 uint64 ↔ Java long). + * Format it as unsigned decimal so verifiers reconstruct the same wrap for + * nonces ≥ 2^63 — otherwise `Long.toString` emits a negative number and + * signature verification fails. */ fun wrapMessage(nonce: Long, message: ByteArray, source: String): String { val sha256 = MessageDigest.getInstance("SHA-256") - val formatterMsg = StringBuilder(11 + 1 + 18 + 1 + 64 + 1 + 64) + val formatterMsg = StringBuilder(11 + 1 + 20 + 1 + 64 + 1 + 64) formatterMsg.append(MSG_PREFIX) .append(MSG_SEPARATOR) - .append(nonce.toString()) + .append(java.lang.Long.toUnsignedString(nonce)) .append(MSG_SEPARATOR) .append(source) .append(MSG_SEPARATOR) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/RsaSignerSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/RsaSignerSpec.groovy index 2e212a45..d73a1a43 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/RsaSignerSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/signature/RsaSignerSpec.groovy @@ -50,6 +50,43 @@ class RsaSignerSpec extends Specification { sig.keyId == 100L } + def "Wrap nonce >= 2^63 as unsigned"() { + setup: + def signer = new RsaSigner(Stub(RSAPrivateKey), 100L) + // bit pattern of 12241848404401059555 (uint64) == -6204895669308492061 (signed Long) + def nonceBits = -6204895669308492061L + + when: + def act = signer.wrapMessage(nonceBits, "test".bytes, "infura") + + then: + act == "DSHACKLESIG/12241848404401059555/infura/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + !act.contains("-") + } + + def "Signed message is valid for nonce >= 2^63"() { + setup: + def result = "test".bytes + def keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(2048) + def pair = keyGen.generateKeyPair() + // bit pattern of 12241848404401059555 (uint64) == -6204895669308492061 (signed Long) + def nonceBits = -6204895669308492061L + + def sha256 = MessageDigest.getInstance("SHA-256") + def verifier = Signature.getInstance("SHA256withRSA", "BC") + verifier.initVerify(pair.getPublic()) + verifier.update("DSHACKLESIG/12241848404401059555/infura/${Hex.encodeHexString(sha256.digest(result))}".getBytes()) + + def signer = new RsaSigner((pair.getPrivate() as RSAPrivateKey), 100L) + + when: + def sig = signer.sign(nonceBits, result, "infura") + + then: + verifier.verify(sig.value) + } + def "Signer is enabled"() { setup: def signer = new RsaSigner(Stub(RSAPrivateKey), 1L)