aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2025-04-14 09:21:01 -0400
committerXe Iaso <me@xeiaso.net>2025-04-14 09:21:01 -0400
commitd96074a82ea22186a206e281951a37133247c134 (patch)
tree81138ab4d8df39104a5b144ab675316c931ce2a5
parent95f70ddf21f7b845ab84d1d070e511da54a91df4 (diff)
downloadanubis-d96074a82ea22186a206e281951a37133247c134.tar.xz
anubis-d96074a82ea22186a206e281951a37133247c134.zip
lib: enable wasm based check validation
Signed-off-by: Xe Iaso <me@xeiaso.net>
-rw-r--r--anubis.go2
-rw-r--r--cmd/anubis/main.go8
-rw-r--r--lib/anubis.go82
-rw-r--r--lib/anubis_test.go2
-rw-r--r--lib/policy/config/config.go14
-rw-r--r--lib/policy/policy.go8
-rw-r--r--package-lock.json4
-rw-r--r--package.json2
-rw-r--r--web/js/algos/argon2id.mjs159
-rw-r--r--web/js/algos/sha256.mjs1
-rw-r--r--web/js/main.mjs2
11 files changed, 249 insertions, 35 deletions
diff --git a/anubis.go b/anubis.go
index b184a45..caa743a 100644
--- a/anubis.go
+++ b/anubis.go
@@ -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
diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go
index 724f88a..e0ecea5 100644
--- a/cmd/anubis/main.go
+++ b/cmd/anubis/main.go
@@ -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)
diff --git a/lib/anubis.go b/lib/anubis.go
index 6fd18a5..74d096f 100644
--- a/lib/anubis.go
+++ b/lib/anubis.go
@@ -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")
+ 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
+ }
+
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
+ }
- nonce, err := strconv.Atoi(nonceStr)
+ 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)
+ validator, ok := s.validators[string(rule.Challenge.Algorithm)]
+ if !ok {
+ s.ClearCookie(w)
+ 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
+ }
- if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
+ ok, err = validator.Verify(r.Context(), challengeBytes, responseBytes, nonce, rule.Challenge.Difficulty)
+ if err != nil {
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("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
}
- // compare the leading zeroes
- if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
+ if !ok {
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("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
}
diff --git a/lib/anubis_test.go b/lib/anubis_test.go
index 1e0cdf2..48343a2 100644
--- a/lib/anubis_test.go
+++ b/lib/anubis_test.go
@@ -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 {
diff --git a/lib/policy/config/config.go b/lib/policy/config/config.go
index e8f5161..40b4fe9 100644
--- a/lib/policy/config/config.go
+++ b/lib/policy/config/config.go
@@ -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))
diff --git a/lib/policy/policy.go b/lib/policy/policy.go
index 3f80fa3..ba7e44c 100644
--- a/lib/policy/policy.go
+++ b/lib/policy/policy.go
@@ -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
}
}
diff --git a/package-lock.json b/package-lock.json
index e94f885..99b7bde 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index 5ab30ab..fc531cd 100644
--- a/package.json
+++ b/package.json
@@ -24,4 +24,4 @@
"postcss-import-url": "^7.2.0",
"postcss-url": "^10.1.3"
}
-}
+} \ No newline at end of file
diff --git a/web/js/algos/argon2id.mjs b/web/js/algos/argon2id.mjs
new file mode 100644
index 0000000..4e467bd
--- /dev/null
+++ b/web/js/algos/argon2id.mjs
@@ -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();
+}
+
diff --git a/web/js/algos/sha256.mjs b/web/js/algos/sha256.mjs
index cfea876..36f9bb4 100644
--- a/web/js/algos/sha256.mjs
+++ b/web/js/algos/sha256.mjs
@@ -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;
diff --git a/web/js/main.mjs b/web/js/main.mjs
index 79d62e9..ee43e36 100644
--- a/web/js/main.mjs
+++ b/web/js/main.mjs
@@ -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,