commit 87c9871: [Feature] Support segwit BTC addresses, fix LTC verification
Vsevolod Stakhov
vsevolod at highsecure.ru
Wed Oct 23 14:56:09 UTC 2019
Author: Vsevolod Stakhov
Date: 2019-10-23 15:53:27 +0100
URL: https://github.com/rspamd/rspamd/commit/87c987155c06650750c355e8a0fd08299692a498 (HEAD -> master)
[Feature] Support segwit BTC addresses, fix LTC verification
---
rules/bitcoin.lua | 154 +++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 123 insertions(+), 31 deletions(-)
diff --git a/rules/bitcoin.lua b/rules/bitcoin.lua
index 0b611796d..86380badf 100644
--- a/rules/bitcoin.lua
+++ b/rules/bitcoin.lua
@@ -17,6 +17,7 @@ limitations under the License.
-- Bitcoin filter rules
local fun = require "fun"
+local bit = require "bit"
local off = 0
local base58_dec = fun.tomap(fun.map(
function(c)
@@ -25,16 +26,127 @@ local base58_dec = fun.tomap(fun.map(
end,
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"))
+local function is_traditional_btc_address(word)
+ local hash = require "rspamd_cryptobox_hash"
+
+ local bytes = {}
+ for i=1,25 do bytes[i] = 0 end
+ -- Base58 decode loop
+ fun.each(function(ch)
+ local acc = base58_dec[ch] or 0
+ for i=25,1,-1 do
+ acc = acc + (58 * bytes[i]);
+ bytes[i] = acc % 256
+ acc = math.floor(acc / 256);
+ end
+ end, word)
+ -- Now create a validation tag
+ local sha256 = hash.create_specific('sha256')
+ for i=1,21 do
+ sha256:update(string.char(bytes[i]))
+ end
+ sha256 = hash.create_specific('sha256', sha256:bin()):bin()
+
+ -- Compare tags
+ local valid = true
+ for i=1,4 do
+ if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
+ valid = false
+ end
+ end
+
+ return valid
+end
+
+-- Beach32 checksum combiner
+local function polymod(...)
+ local chk = 1;
+ local gen = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
+ for _,t in ipairs({...}) do
+ for _,v in ipairs(t) do
+ local top = bit.rshift(chk, 25)
+
+ chk = bit.bxor(bit.lshift(bit.band(chk, 0x1ffffff), 5), v)
+ for i=1,5 do
+ if bit.band(bit.rshift(top, i - 1), 0x1) ~= 0 then
+ chk = bit.bxor(chk, gen[i])
+ end
+ end
+ end
+ end
+
+ return chk
+end
+
+-- Beach32 expansion function
+local function hrpExpand(hrp)
+ local ret = {}
+ fun.each(function(byte)
+ ret[#ret + 1] = bit.rshift(byte, 5)
+ end, fun.map(string.byte, fun.iter(hrp)))
+ ret[#ret + 1] = 0
+ fun.each(function(byte)
+ ret[#ret + 1] = bit.band(byte, 0x1f)
+ end, fun.map(string.byte, fun.iter(hrp)))
+
+ return ret
+end
+
+local function verify_beach32_cksum(hrp, elts)
+ return polymod(hrpExpand(hrp), elts) == 1
+end
+
+local function is_segwit_bech32_address(word)
+ local has_upper, has_lower, has_invalid
+
+ if #word > 90 then return false end
+
+ fun.each(function(ch)
+ if ch < 33 or ch > 126 then
+ has_invalid = true
+ elseif ch >= 97 and ch <= 122 then
+ has_lower = true
+ elseif ch >= 65 and ch <= 90 then
+ has_upper = true;
+ end
+ end, fun.map(string.byte, fun.iter(word)))
+
+ if has_invalid or (has_lower and has_upper) then
+ return false
+ end
+
+ word = word:lower()
+ local last_one_pos = word:find('1[^1]*$')
+ if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #word) then
+ return false
+ end
+ local hrp = word:sub(1, last_one_pos - 1)
+ local d = {}
+ local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
+ for i=last_one_pos + 1,#word do
+ local c = word:sub(i, i)
+ local pos = charset:find(c)
+
+ if not pos then
+ return false
+ end
+ d[#d + 1] = pos - 1
+ end
+
+ return verify_beach32_cksum(hrp, d)
+end
+
+
rspamd_config:register_symbol{
name = 'BITCOIN_ADDR',
description = 'Message has a valid bitcoin wallet address',
callback = function(task)
local rspamd_re = require "rspamd_regexp"
- local hash = require "rspamd_cryptobox_hash"
- local btc_wallet_re = rspamd_re.create_cached('^[13][1-9A-Za-z]{25,34}$')
- local ltc_wallet_re = rspamd_re.create_cached('^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$')
+ local btc_wallet_re = rspamd_re.create_cached('^[13LM][1-9A-Za-z]{25,34}$')
+ local segwit_wallet_re = rspamd_re.create_cached('^[b][c]1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,74}$', 'i')
local words_matched = {}
+ local segwit_words_matched = {}
local valid_wallets = {}
for _,part in ipairs(task:get_text_parts() or {}) do
@@ -46,42 +158,22 @@ rspamd_config:register_symbol{
end
end
- pw = part:filter_words(ltc_wallet_re, 'raw', 3)
+ pw = part:filter_words(segwit_wallet_re, 'raw', 3)
if pw and #pw > 0 then
for _,w in ipairs(pw) do
- -- Do not validate, LTC regexp is more strict than BTC one, maybe do it in future
- valid_wallets[#valid_wallets + 1] = w
+ segwit_words_matched[#segwit_words_matched + 1] = w
end
end
end
for _,word in ipairs(words_matched) do
- local bytes = {}
- for i=1,25 do bytes[i] = 0 end
- -- Base58 decode loop
- fun.each(function(ch)
- local acc = base58_dec[ch] or 0
- for i=25,1,-1 do
- acc = acc + (58 * bytes[i]);
- bytes[i] = acc % 256
- acc = math.floor(acc / 256);
- end
- end, word)
- -- Now create a validation tag
- local sha256 = hash.create_specific('sha256')
- for i=1,21 do
- sha256:update(string.char(bytes[i]))
- end
- sha256 = hash.create_specific('sha256', sha256:bin()):bin()
-
- -- Compare tags
- local valid = true
- for i=1,4 do
- if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
- valid = false
- end
+ local valid = is_traditional_btc_address(word)
+ if valid then
+ valid_wallets[#valid_wallets + 1] = word
end
-
+ end
+ for _,word in ipairs(segwit_words_matched) do
+ local valid = is_segwit_bech32_address(word)
if valid then
valid_wallets[#valid_wallets + 1] = word
end
More information about the Commits
mailing list