package io.emeraldpay.dshackle.upstream.signature import org.apache.commons.codec.binary.Hex import java.security.MessageDigest import java.security.Signature import java.security.interfaces.ECPrivateKey class EcdsaSigner( private val privateKey: ECPrivateKey, val keyId: Long, ) : ResponseSigner { companion object { const val SIGN_SCHEME = "SHA256withECDSA" const val MSG_PREFIX = "DSHACKLESIG" const val MSG_SEPARATOR = '/' } override fun sign(nonce: Long, message: ByteArray, source: String): ResponseSigner.Signature { val sig = Signature.getInstance(SIGN_SCHEME, "BC") sig.initSign(privateKey) val wrapped = wrapMessage(nonce, message, source) sig.update(wrapped.toByteArray()) val value = sig.sign() return ResponseSigner.Signature(value, source, keyId) } /** * To avoid various attacks, such as various kinds of padding and message alternating attacks, * we (1) tag the original message to specify the source, (2) ensure no parts of the message can affect each other * and (3) ensure the message cannot break the wrapping. * * We're doing that by converting the message as `"DSHACKLESIG/" || str(nonce) || "/" || hex(sha256(msg))` * * I.e.: * - three elements in the wrapped message * - separated by "/" which is not a part of any element * - first element is DSHACKLESIG tag * - second is the nonce value encode as decimal string * - third is SHA256 hash of the original message encoded as hex string */ fun wrapMessage(nonce: Long, message: ByteArray, source: String): String { val sha256 = MessageDigest.getInstance("SHA-256") // we create it with max capacity that we expect for the result, which is total lengths of its parts val formatterMsg = StringBuilder(11 + 1 + 18 + 1 + 64 + 1 + 64) formatterMsg.append(MSG_PREFIX) .append(MSG_SEPARATOR) .append(nonce.toString()) .append(MSG_SEPARATOR) // We expect that the id is short enough (less than 64 symbols) and also it doesn't contain the `/` symbol // which is verified in UpstreamConfigReader and DefaultUpstream constructor .append(source) .append(MSG_SEPARATOR) .append(Hex.encodeHexString(sha256.digest(message))) return formatterMsg.toString() } }