Files
dshackle/src/main/kotlin/io/emeraldpay/dshackle/upstream/signature/EcdsaSigner.kt
2023-01-18 14:49:57 +04:00

58 lines
2.4 KiB
Kotlin

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()
}
}