lib: enable wasm based check validation

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso
2025-04-14 09:21:01 -04:00
parent 95f70ddf21
commit d96074a82e
11 changed files with 251 additions and 37 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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
View File

@@ -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": {

View File

@@ -24,4 +24,4 @@
"postcss-import-url": "^7.2.0",
"postcss-url": "^10.1.3"
}
}
}

159
web/js/algos/argon2id.mjs Normal file
View 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();
}

View File

@@ -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;

View File

@@ -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,