feat(lib/challenge/wasm): server side validation logic

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso
2025-09-28 02:35:42 +00:00
parent a63cbc7ced
commit 8999303eef
9 changed files with 51 additions and 185 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ type ValidateInput struct {
type Impl interface { type Impl interface {
// Setup registers any additional routes with the Impl for assets or API routes. // Setup registers any additional routes with the Impl for assets or API routes.
Setup(mux *http.ServeMux) Setup(mux *http.ServeMux) error
// Issue a new challenge to the user, called by the Anubis. // Issue a new challenge to the user, called by the Anubis.
Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)
+1 -1
View File
@@ -21,7 +21,7 @@ func init() {
type Impl struct{} type Impl struct{}
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
+1 -1
View File
@@ -36,7 +36,7 @@ func init() {
type impl struct{} type impl struct{}
func (i *impl) Setup(mux *http.ServeMux) {} func (i *impl) Setup(mux *http.ServeMux) error { return nil }
func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) { func (i *impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *challenge.IssueInput) (templ.Component, error) {
u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge") u, err := r.URL.Parse(anubis.BasePrefix + "/.within.website/x/cmd/anubis/api/pass-challenge")
+1 -1
View File
@@ -25,7 +25,7 @@ type Impl struct {
Algorithm string Algorithm string
} }
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) error { return nil }
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)
-151
View File
@@ -1,151 +0,0 @@
package wasm
import (
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/policy"
"github.com/TecharoHQ/anubis/lib/policy/config"
)
func mkRequest(t *testing.T, values map[string]string) *http.Request {
t.Helper()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
q := req.URL.Query()
for k, v := range values {
q.Set(k, v)
}
req.URL.RawQuery = q.Encode()
return req
}
func TestBasic(t *testing.T) {
i := &Impl{Algorithm: "fast"}
bot := &policy.Bot{
Challenge: &config.ChallengeRules{
Algorithm: "fast",
Difficulty: 0,
ReportAs: 0,
},
}
const challengeStr = "hunter"
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
for _, cs := range []struct {
name string
req *http.Request
err error
challengeStr string
}{
{
name: "allgood",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
"response": response,
}),
err: nil,
challengeStr: challengeStr,
},
{
name: "no-params",
req: mkRequest(t, map[string]string{}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-nonce",
req: mkRequest(t, map[string]string{
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-elapsedTime",
req: mkRequest(t, map[string]string{
"nonce": "0",
"response": response,
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "missing-response",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
}),
err: challenge.ErrMissingField,
challengeStr: challengeStr,
},
{
name: "wrong-nonce-format",
req: mkRequest(t, map[string]string{
"nonce": "taco",
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrInvalidFormat,
challengeStr: challengeStr,
},
{
name: "wrong-elapsedTime-format",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "taco",
"response": response,
}),
err: challenge.ErrInvalidFormat,
challengeStr: challengeStr,
},
{
name: "invalid-response",
req: mkRequest(t, map[string]string{
"nonce": "0",
"elapsedTime": "69",
"response": response,
}),
err: challenge.ErrFailed,
challengeStr: "Tacos are tasty",
},
} {
t.Run(cs.name, func(t *testing.T) {
lg := slog.With()
i.Setup(http.NewServeMux())
inp := &challenge.IssueInput{
Rule: bot,
Challenge: &challenge.Challenge{
RandomData: cs.challengeStr,
},
}
if _, err := i.Issue(httptest.NewRecorder(), cs.req, lg, inp); err != nil {
t.Errorf("can't issue challenge: %v", err)
}
if err := i.Validate(cs.req, lg, &challenge.ValidateInput{
Rule: bot,
Challenge: &challenge.Challenge{
RandomData: cs.challengeStr,
},
}); !errors.Is(err, cs.err) {
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
}
})
}
}
@@ -1,31 +1,44 @@
package wasm package wasm
import ( import (
"crypto/subtle" "context"
"encoding/hex"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/TecharoHQ/anubis/internal"
chall "github.com/TecharoHQ/anubis/lib/challenge" chall "github.com/TecharoHQ/anubis/lib/challenge"
"github.com/TecharoHQ/anubis/lib/localization" "github.com/TecharoHQ/anubis/lib/localization"
"github.com/TecharoHQ/anubis/wasm"
"github.com/TecharoHQ/anubis/web"
"github.com/a-h/templ" "github.com/a-h/templ"
) )
//go:generate go tool github.com/a-h/templ/cmd/templ generate //go:generate go tool github.com/a-h/templ/cmd/templ generate
func init() { func init() {
chall.Register("argon2id", &Impl{Algorithm: "argon2id"}) chall.Register("argon2id", &Impl{algorithm: "argon2id"})
chall.Register("sha256", &Impl{Algorithm: "sha256"}) chall.Register("sha256", &Impl{algorithm: "sha256"})
} }
type Impl struct { type Impl struct {
Algorithm string algorithm string
runner *wasm.Runner
} }
func (i *Impl) Setup(mux *http.ServeMux) {} func (i *Impl) Setup(mux *http.ServeMux) error {
fname := fmt.Sprintf("static/wasm/simd128/%s.wasm", i.algorithm)
fin, err := web.Static.Open(fname)
if err != nil {
return err
}
defer fin.Close()
i.runner, err = wasm.NewRunner(context.Background(), fname, fin)
return err
}
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) { func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
loc := localization.GetLocalizer(r) loc := localization.GetLocalizer(r)
@@ -33,9 +46,6 @@ func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in
} }
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error { func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
rule := in.Rule
challenge := in.Challenge.RandomData
nonceStr := r.FormValue("nonce") nonceStr := r.FormValue("nonce")
if nonceStr == "" { if nonceStr == "" {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField)) return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
@@ -62,20 +72,27 @@ func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInpu
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField)) return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
} }
calcString := fmt.Sprintf("%s%d", challenge, nonce) challengeBytes, err := hex.DecodeString(in.Challenge.RandomData)
calculated := internal.SHA256sum(calcString) if err != nil {
return chall.NewError("validate", "invalid random data", fmt.Errorf("can't decode random data: %w", err))
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
} }
// compare the leading zeroes gotBytes, err := hex.DecodeString(response)
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) { if err != nil {
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response)) return chall.NewError("validate", "invalid client data format", fmt.Errorf("%w response", chall.ErrInvalidFormat))
}
ok, err := i.runner.Verify(r.Context(), challengeBytes, gotBytes, uint32(nonce), uint32(in.Rule.Challenge.Difficulty))
if err != nil {
return chall.NewError("validate", "internal WASM error", fmt.Errorf("can't run wasm validation logic: %w", err))
}
if !ok {
return chall.NewError("verify", "client calculated wrong data", fmt.Errorf("%w: response invalid: %s", chall.ErrFailed, response))
} }
lg.Debug("challenge took", "elapsedTime", elapsedTime) lg.Debug("challenge took", "elapsedTime", elapsedTime)
chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime) chall.TimeTaken.WithLabelValues(i.algorithm).Observe(elapsedTime)
return nil return nil
} }
@@ -41,7 +41,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 10, Col: 165} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 10, Col: 165}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -54,7 +54,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 11, Col: 174} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 11, Col: 174}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -67,7 +67,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading")) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 12, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 12, Col: 41}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -80,7 +80,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 13, Col: 136} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 13, Col: 136}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -98,7 +98,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("simplified_explanation")) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("simplified_explanation"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 20, Col: 44} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 20, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -116,7 +116,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation")) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 24, Col: 46} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 24, Col: 46}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -129,7 +129,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var8 string var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise")) templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 27, Col: 39} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 27, Col: 39}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -142,7 +142,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose")) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 30, Col: 34} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 30, Col: 34}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -155,7 +155,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note")) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 33, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 33, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -173,7 +173,7 @@ func page(localizer *localization.SimpleLocalizer) templ.Component {
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("javascript_required")) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("javascript_required"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 39, Col: 40} return templ.Error{Err: templ_7745c5c3_Err, FileName: `wasm.templ`, Line: 39, Col: 40}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -49,7 +49,7 @@ for file in js/**/*.ts js/**/*.mjs; do
mkdir -p "$(dirname "$out")" mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --bundle --outfile="$out" --banner:js="$LICENSE" esbuild "$file" --sourcemap --minify --bundle --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out" gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out" zstd -f -k --ultra -22 "$out"
brotli -fZk "$out" brotli -fZk "$out"