58 lines
2.4 KiB
Kotlin
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()
|
|
}
|
|
}
|