mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-10 18:48:44 +00:00
lib: enable wasm based check validation
Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
@@ -16,4 +16,4 @@ const StaticPath = "/.within.website/x/cmd/anubis/"
|
||||
|
||||
// DefaultDifficulty is the default "difficulty" (number of leading zeroes)
|
||||
// that must be met by the client in order to pass the challenge.
|
||||
const DefaultDifficulty = 4
|
||||
const DefaultDifficulty uint32 = 4
|
||||
|
||||
@@ -40,7 +40,7 @@ import (
|
||||
var (
|
||||
bind = flag.String("bind", ":8923", "network address to bind HTTP to")
|
||||
bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp")
|
||||
challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge")
|
||||
challengeDifficulty = flag.Int("difficulty", int(anubis.DefaultDifficulty), "difficulty of the challenge")
|
||||
cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for")
|
||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||
@@ -58,7 +58,7 @@ var (
|
||||
ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough")
|
||||
ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time")
|
||||
extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder")
|
||||
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
||||
webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals")
|
||||
)
|
||||
|
||||
func keyFromHex(value string) (ed25519.PrivateKey, error) {
|
||||
@@ -194,7 +194,7 @@ func main() {
|
||||
log.Fatalf("can't make reverse proxy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := libanubis.LoadPoliciesOrDefault(*policyFname, *challengeDifficulty)
|
||||
policy, err := libanubis.LoadPoliciesOrDefault(*policyFname, uint32(*challengeDifficulty))
|
||||
if err != nil {
|
||||
log.Fatalf("can't parse policy file: %v", err)
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func main() {
|
||||
OGPassthrough: *ogPassthrough,
|
||||
OGTimeToLive: *ogTimeToLive,
|
||||
Target: *target,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
WebmasterEmail: *webmasterEmail,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -31,6 +35,7 @@ import (
|
||||
"github.com/TecharoHQ/anubis/internal/ogtags"
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/wasm"
|
||||
"github.com/TecharoHQ/anubis/web"
|
||||
"github.com/TecharoHQ/anubis/xess"
|
||||
)
|
||||
@@ -80,7 +85,7 @@ type Options struct {
|
||||
WebmasterEmail string
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
func LoadPoliciesOrDefault(fname string, defaultDifficulty uint32) (*policy.ParsedConfig, error) {
|
||||
var fin io.ReadCloser
|
||||
var err error
|
||||
|
||||
@@ -122,6 +127,32 @@ func New(opts Options) (*Server, error) {
|
||||
opts: opts,
|
||||
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
|
||||
OGTags: ogtags.NewOGTagCache(opts.Target, opts.OGPassthrough, opts.OGTimeToLive),
|
||||
validators: map[string]Verifier{
|
||||
"fast": VerifierFunc(BasicSHA256Verify),
|
||||
"slow": VerifierFunc(BasicSHA256Verify),
|
||||
},
|
||||
}
|
||||
|
||||
finfos, err := fs.ReadDir(web.Static, "static/wasm")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[unexpected] can't read any webassembly files in the static folder: %w", err)
|
||||
}
|
||||
|
||||
for _, finfo := range finfos {
|
||||
fin, err := web.Static.Open("static/wasm/" + finfo.Name())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[unexpected] can't read static/wasm/%s: %w", finfo.Name(), err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
name := strings.TrimSuffix(finfo.Name(), filepath.Ext(finfo.Name()))
|
||||
|
||||
runner, err := wasm.NewRunner(context.Background(), finfo.Name(), fin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load static/wasm/%s: %w", finfo.Name(), err)
|
||||
}
|
||||
|
||||
result.validators[name] = runner
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@@ -161,13 +192,15 @@ type Server struct {
|
||||
opts Options
|
||||
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
|
||||
OGTags *ogtags.OGTagCache
|
||||
|
||||
validators map[string]Verifier
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
|
||||
func (s *Server) challengeFor(r *http.Request, difficulty uint32) string {
|
||||
fp := sha256.Sum256(s.priv.Seed())
|
||||
|
||||
challengeData := fmt.Sprintf(
|
||||
@@ -404,7 +437,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Internal Server Error: administrator has misconfigured Anubis. Please contact the administrator and ask them to look for the logs around \"passChallenge\".", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
lg = lg.With("check_result", cr)
|
||||
lg = lg.With("check_result", cr, "algorithm", rule.Challenge.Algorithm)
|
||||
|
||||
nonceStr := r.FormValue("nonce")
|
||||
if nonceStr == "" {
|
||||
@@ -436,33 +469,52 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
response := r.FormValue("response")
|
||||
redir := r.FormValue("redir")
|
||||
|
||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
responseBytes, err := hex.DecodeString(response)
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("response doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response format", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
nonce, err := strconv.Atoi(nonceStr)
|
||||
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
|
||||
challengeBytes, err := hex.DecodeString(challenge)
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("challenge doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid internal challenge format", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
nonceRaw, err := strconv.ParseUint(nonceStr, 10, 32)
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("nonce doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid nonce", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
nonce := uint32(nonceRaw)
|
||||
|
||||
calcString := fmt.Sprintf("%s%d", challenge, nonce)
|
||||
calculated := internal.SHA256sum(calcString)
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||
validator, ok := s.validators[string(rule.Challenge.Algorithm)]
|
||||
if !ok {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("hash does not match", "got", response, "want", calculated)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||
failedValidations.Inc()
|
||||
lg.Debug("nonce doesn't parse", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage(fmt.Sprintf("Internal anubis error has been detected and you cannot proceed. Tried to look up a validator for algorithm %s but wasn't able to find one. Please contact the administrator of this instance of anubis", rule.Challenge.Algorithm), s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// compare the leading zeroes
|
||||
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||
ok, err = validator.Verify(r.Context(), challengeBytes, responseBytes, nonce, rule.Challenge.Difficulty)
|
||||
if err != nil {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r)
|
||||
failedValidations.Inc()
|
||||
lg.Debug("verification error", "err", err)
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Your challenge failed validation. Please go back and try your challenge again", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusBadRequest)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
s.ClearCookie(w)
|
||||
lg.Debug("response invalid")
|
||||
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Your challenge failed validation. Please go back and try your challenge again", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusBadRequest)).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||
fmt.Fprintln(w, "OK")
|
||||
})
|
||||
|
||||
for i := 1; i < 10; i++ {
|
||||
for i := uint32(1); i < 10; i++ {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
anubisPolicy, err := LoadPoliciesOrDefault("", i)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,9 +31,11 @@ const (
|
||||
type Algorithm string
|
||||
|
||||
const (
|
||||
AlgorithmUnknown Algorithm = ""
|
||||
AlgorithmFast Algorithm = "fast"
|
||||
AlgorithmSlow Algorithm = "slow"
|
||||
AlgorithmUnknown Algorithm = ""
|
||||
AlgorithmFast Algorithm = "fast"
|
||||
AlgorithmSlow Algorithm = "slow"
|
||||
AlgorithmArgon2ID Algorithm = "argon2id"
|
||||
AlgorithmSHA256 Algorithm = "sha256"
|
||||
)
|
||||
|
||||
type BotConfig struct {
|
||||
@@ -101,8 +103,8 @@ func (b BotConfig) Valid() error {
|
||||
}
|
||||
|
||||
type ChallengeRules struct {
|
||||
Difficulty int `json:"difficulty"`
|
||||
ReportAs int `json:"report_as"`
|
||||
Difficulty uint32 `json:"difficulty"`
|
||||
ReportAs uint32 `json:"report_as"`
|
||||
Algorithm Algorithm `json:"algorithm"`
|
||||
}
|
||||
|
||||
@@ -124,7 +126,7 @@ func (cr ChallengeRules) Valid() error {
|
||||
}
|
||||
|
||||
switch cr.Algorithm {
|
||||
case AlgorithmFast, AlgorithmSlow, AlgorithmUnknown:
|
||||
case AlgorithmFast, AlgorithmSlow, AlgorithmArgon2ID, AlgorithmSHA256, AlgorithmUnknown:
|
||||
// do nothing, it's all good
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("%w: %q", ErrChallengeRuleHasWrongAlgorithm, cr.Algorithm))
|
||||
|
||||
@@ -27,7 +27,7 @@ type ParsedConfig struct {
|
||||
|
||||
Bots []Bot
|
||||
DNSBL bool
|
||||
DefaultDifficulty int
|
||||
DefaultDifficulty uint32
|
||||
}
|
||||
|
||||
func NewParsedConfig(orig config.Config) *ParsedConfig {
|
||||
@@ -36,7 +36,7 @@ func NewParsedConfig(orig config.Config) *ParsedConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedConfig, error) {
|
||||
func ParseConfig(fin io.Reader, fname string, defaultDifficulty uint32) (*ParsedConfig, error) {
|
||||
var c config.Config
|
||||
if err := json.NewDecoder(fin).Decode(&c); err != nil {
|
||||
return nil, fmt.Errorf("can't parse policy config JSON %s: %w", fname, err)
|
||||
@@ -99,12 +99,12 @@ func ParseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
|
||||
parsedBot.Challenge = &config.ChallengeRules{
|
||||
Difficulty: defaultDifficulty,
|
||||
ReportAs: defaultDifficulty,
|
||||
Algorithm: config.AlgorithmFast,
|
||||
Algorithm: config.AlgorithmArgon2ID,
|
||||
}
|
||||
} else {
|
||||
parsedBot.Challenge = b.Challenge
|
||||
if parsedBot.Challenge.Algorithm == config.AlgorithmUnknown {
|
||||
parsedBot.Challenge.Algorithm = config.AlgorithmFast
|
||||
parsedBot.Challenge.Algorithm = config.AlgorithmArgon2ID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@xeserv/xess",
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.0.0-see-VERSION-file",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@xeserv/xess",
|
||||
"name": "@techaro/anubis",
|
||||
"version": "1.0.0-see-VERSION-file",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
"postcss-import-url": "^7.2.0",
|
||||
"postcss-url": "^10.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
159
web/js/algos/argon2id.mjs
Normal file
159
web/js/algos/argon2id.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import { u } from "../xeact.mjs";
|
||||
|
||||
export default function process(
|
||||
data,
|
||||
difficulty = 16,
|
||||
signal = null,
|
||||
pc = null,
|
||||
threads = (navigator.hardwareConcurrency || 1),
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let webWorkerURL = URL.createObjectURL(new Blob([
|
||||
'(', processTask(), ')()'
|
||||
], { type: 'application/javascript' }));
|
||||
|
||||
const module = await fetch(u("/.within.website/x/cmd/anubis/static/wasm/argon2id.wasm"))
|
||||
.then(resp => WebAssembly.compileStreaming(resp));
|
||||
|
||||
const workers = [];
|
||||
const terminate = () => {
|
||||
workers.forEach((w) => w.terminate());
|
||||
if (signal != null) {
|
||||
// clean up listener to avoid memory leak
|
||||
signal.removeEventListener("abort", terminate);
|
||||
if (signal.aborted) {
|
||||
console.log("PoW aborted");
|
||||
reject(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (signal != null) {
|
||||
signal.addEventListener("abort", terminate, { once: true });
|
||||
}
|
||||
|
||||
for (let i = 0; i < threads; i++) {
|
||||
let worker = new Worker(webWorkerURL);
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
if (typeof event.data === "number") {
|
||||
pc?.(event.data);
|
||||
} else {
|
||||
terminate();
|
||||
resolve(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (event) => {
|
||||
terminate();
|
||||
reject(event);
|
||||
};
|
||||
|
||||
worker.postMessage({
|
||||
data,
|
||||
difficulty,
|
||||
nonce: i,
|
||||
threads,
|
||||
module,
|
||||
});
|
||||
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(webWorkerURL);
|
||||
});
|
||||
}
|
||||
|
||||
function processTask() {
|
||||
return function () {
|
||||
addEventListener('message', async (event) => {
|
||||
const importObject = {
|
||||
anubis: {
|
||||
anubis_update_nonce: (nonce) => postMessage(nonce),
|
||||
}
|
||||
};
|
||||
|
||||
const instance = await WebAssembly.instantiate(event.data.module, importObject);
|
||||
|
||||
// Get exports
|
||||
const {
|
||||
anubis_work,
|
||||
data_ptr,
|
||||
result_hash_ptr,
|
||||
result_hash_size,
|
||||
set_data_length,
|
||||
memory
|
||||
} = instance.exports;
|
||||
|
||||
function uint8ArrayToHex(arr) {
|
||||
return Array.from(arr)
|
||||
.map((c) => c.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function hexToUint8Array(hexString) {
|
||||
// Remove whitespace and optional '0x' prefix
|
||||
hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
|
||||
|
||||
// Check for valid length
|
||||
if (hexString.length % 2 !== 0) {
|
||||
throw new Error('Invalid hex string length');
|
||||
}
|
||||
|
||||
// Check for valid characters
|
||||
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
|
||||
throw new Error('Invalid hex characters');
|
||||
}
|
||||
|
||||
// Convert to Uint8Array
|
||||
const byteArray = new Uint8Array(hexString.length / 2);
|
||||
for (let i = 0; i < byteArray.length; i++) {
|
||||
const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
|
||||
byteArray[i] = byteValue;
|
||||
}
|
||||
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
// Write data to buffer
|
||||
function writeToBuffer(data) {
|
||||
if (data.length > 1024) throw new Error("Data exceeds buffer size");
|
||||
|
||||
// Get pointer and create view
|
||||
const offset = data_ptr();
|
||||
const buffer = new Uint8Array(memory.buffer, offset, data.length);
|
||||
|
||||
// Copy data
|
||||
buffer.set(data);
|
||||
|
||||
// Set data length
|
||||
set_data_length(data.length);
|
||||
}
|
||||
|
||||
function readFromChallenge() {
|
||||
const offset = result_hash_ptr();
|
||||
const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
let data = event.data.data;
|
||||
let difficulty = event.data.difficulty;
|
||||
let nonce = event.data.nonce;
|
||||
let interand = event.data.threads;
|
||||
|
||||
writeToBuffer(hexToUint8Array(data));
|
||||
|
||||
nonce = anubis_work(difficulty, nonce, interand);
|
||||
const challenge = readFromChallenge();
|
||||
|
||||
data = uint8ArrayToHex(challenge);
|
||||
|
||||
postMessage({
|
||||
hash: data,
|
||||
difficulty,
|
||||
nonce,
|
||||
});
|
||||
});
|
||||
}.toString();
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ function processTask() {
|
||||
|
||||
let data = event.data.data;
|
||||
let difficulty = event.data.difficulty;
|
||||
let hash;
|
||||
let nonce = event.data.nonce;
|
||||
let interand = event.data.threads;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import argon2id from "./algos/argon2id.mjs";
|
||||
import fast from "./algos/fast.mjs";
|
||||
import slow from "./algos/slow.mjs";
|
||||
import sha256 from "./algos/sha256.mjs";
|
||||
@@ -5,6 +6,7 @@ import { testVideo } from "./video.mjs";
|
||||
import { u } from "./xeact.mjs";
|
||||
|
||||
const algorithms = {
|
||||
"argon2id": argon2id,
|
||||
"fast": fast,
|
||||
"slow": slow,
|
||||
"sha256": sha256,
|
||||
|
||||
Reference in New Issue
Block a user