challenges: prevent unbounded growth of stored cookies by bundling all state onto a single JWT token
This commit is contained in:
@@ -29,6 +29,7 @@ func (a Block) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("Connection", "close")
|
||||
data.ResponseHeaders(w)
|
||||
w.WriteHeader(a.Code)
|
||||
_, _ = w.Write([]byte(fmt.Errorf("access blocked: blocked by administrative rule %s/%s", data.Id.String(), a.RuleHash).Error()))
|
||||
|
||||
|
@@ -42,6 +42,8 @@ type CodeSettings struct {
|
||||
type Code int
|
||||
|
||||
func (a Code) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) {
|
||||
challenge.RequestDataFromContext(r.Context()).ResponseHeaders(w)
|
||||
|
||||
w.WriteHeader(int(a))
|
||||
return false, nil
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package cookie
|
||||
|
||||
import (
|
||||
"git.gammaspectra.live/git/go-away/lib/challenge"
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -18,18 +17,15 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
|
||||
reg.Class = challenge.ClassBlocking
|
||||
|
||||
reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult {
|
||||
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, nil, expiry, true)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
|
||||
utils.SetCookie(challenge.RequestDataFromContext(r.Context()).CookiePrefix+reg.Name, token, expiry, w, r)
|
||||
data := challenge.RequestDataFromContext(r.Context())
|
||||
data.IssueChallengeToken(reg, key, nil, expiry, true)
|
||||
|
||||
uri, err := challenge.RedirectUrl(r, reg)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
|
||||
data.ResponseHeaders(w)
|
||||
http.Redirect(w, r, uri.String(), http.StatusTemporaryRedirect)
|
||||
return challenge.VerifyResultNone
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
http_cel "codeberg.org/gone/http-cel"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
@@ -9,10 +10,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
"maps"
|
||||
unsaferand "math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/textproto"
|
||||
@@ -38,13 +42,17 @@ func (id RequestId) String() string {
|
||||
}
|
||||
|
||||
type RequestData struct {
|
||||
Id RequestId
|
||||
Time time.Time
|
||||
ChallengeVerify map[Id]VerifyResult
|
||||
ChallengeState map[Id]VerifyState
|
||||
Id RequestId
|
||||
Time time.Time
|
||||
ChallengeVerify map[Id]VerifyResult
|
||||
ChallengeState map[Id]VerifyState
|
||||
ChallengeMap TokenChallengeMap
|
||||
challengeMapModified bool
|
||||
|
||||
RemoteAddress netip.AddrPort
|
||||
State StateInterface
|
||||
CookiePrefix string
|
||||
cookieName string
|
||||
issuedChallenge string
|
||||
|
||||
ExtraHeaders http.Header
|
||||
|
||||
@@ -84,6 +92,11 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
|
||||
if q.Has(QueryArgChallenge) {
|
||||
data.issuedChallenge = q.Get(QueryArgChallenge)
|
||||
}
|
||||
|
||||
// delete query parameters that were set by go-away
|
||||
for k := range q {
|
||||
if strings.HasPrefix(k, QueryArgPrefix) {
|
||||
@@ -95,19 +108,12 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R
|
||||
data.header = http_cel.NewMIMEMap(textproto.MIMEHeader(r.Header))
|
||||
data.opts = make(map[string]string)
|
||||
|
||||
sum := sha256.New()
|
||||
sum.Write([]byte(r.Host))
|
||||
sum.Write([]byte{0})
|
||||
sum.Write(data.NetworkPrefix().AsSlice())
|
||||
sum.Write([]byte{0})
|
||||
sum.Write(state.PublicKey())
|
||||
sum.Write([]byte{0})
|
||||
data.CookiePrefix = utils.CookiePrefix + hex.EncodeToString(sum.Sum(nil)[:6]) + "-"
|
||||
|
||||
r = r.WithContext(context.WithValue(r.Context(), requestDataContextKey{}, &data))
|
||||
r = utils.SetRemoteAddress(r, data.RemoteAddress)
|
||||
data.r = r
|
||||
|
||||
data.cookieName = utils.DefaultCookiePrefix + hex.EncodeToString(data.cookieHostKey()) + "-state"
|
||||
|
||||
return r, &data
|
||||
}
|
||||
|
||||
@@ -194,18 +200,70 @@ func (d *RequestData) BackendHost() (http.Handler, string) {
|
||||
return d.State.GetBackend(host), host
|
||||
}
|
||||
|
||||
func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
var issuedChallenge string
|
||||
if q.Has(QueryArgChallenge) {
|
||||
issuedChallenge = q.Get(QueryArgChallenge)
|
||||
func (d *RequestData) ClearChallengeToken(reg *Registration) {
|
||||
delete(d.ChallengeMap, reg.Name)
|
||||
d.challengeMapModified = true
|
||||
}
|
||||
|
||||
func (d *RequestData) IssueChallengeToken(reg *Registration, key Key, result []byte, until time.Time, ok bool) {
|
||||
d.ChallengeMap[reg.Name] = TokenChallenge{
|
||||
Key: key[:],
|
||||
Result: result,
|
||||
Ok: ok,
|
||||
Expiry: jwt.NumericDate(until.Unix()),
|
||||
IssuedAt: jwt.NumericDate(time.Now().UTC().Unix()),
|
||||
}
|
||||
for _, reg := range d.State.GetChallenges() {
|
||||
key := GetChallengeKeyForRequest(d.State, reg, d.Expiration(reg.Duration), r)
|
||||
verifyResult, verifyState, err := reg.VerifyChallengeToken(d.State.PublicKey(), key, r)
|
||||
d.challengeMapModified = true
|
||||
}
|
||||
|
||||
var ErrVerifyKeyMismatch = errors.New("verify: key mismatch")
|
||||
var ErrVerifyVerifyMismatch = errors.New("verify: verification mismatch")
|
||||
var ErrTokenExpired = errors.New("token: expired")
|
||||
|
||||
func (d *RequestData) VerifyChallengeToken(reg *Registration, token TokenChallenge, expectedKey Key) (VerifyResult, VerifyState, error) {
|
||||
if token.Expiry.Time().Compare(time.Now()) < 0 {
|
||||
return VerifyResultFail, VerifyStateNone, ErrTokenExpired
|
||||
}
|
||||
if token.NotBefore.Time().Compare(time.Now()) > 0 {
|
||||
return VerifyResultFail, VerifyStateNone, errors.New("token not valid yet")
|
||||
}
|
||||
|
||||
if bytes.Compare(expectedKey[:], token.Key) != 0 {
|
||||
return VerifyResultFail, VerifyStateNone, ErrVerifyKeyMismatch
|
||||
}
|
||||
|
||||
if reg.Verify != nil {
|
||||
if unsaferand.Float64() < reg.VerifyProbability {
|
||||
// random spot check
|
||||
if ok, err := reg.Verify(expectedKey, token.Result, d.r); err != nil {
|
||||
return VerifyResultFail, VerifyStateFull, err
|
||||
} else if ok == VerifyResultNotOK {
|
||||
return VerifyResultNotOK, VerifyStateFull, nil
|
||||
} else if !ok.Ok() {
|
||||
return ok, VerifyStateFull, ErrVerifyVerifyMismatch
|
||||
} else {
|
||||
return ok, VerifyStateFull, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !token.Ok {
|
||||
return VerifyResultNotOK, VerifyStateBrief, nil
|
||||
}
|
||||
return VerifyResultOK, VerifyStateBrief, nil
|
||||
}
|
||||
|
||||
func (d *RequestData) verifyChallenge(reg *Registration, key Key) (verifyResult VerifyResult, verifyState VerifyState, err error) {
|
||||
|
||||
token, ok := d.ChallengeMap[reg.Name]
|
||||
if !ok {
|
||||
verifyResult = VerifyResultFail
|
||||
verifyState = VerifyStateNone
|
||||
} else {
|
||||
verifyResult, verifyState, err = d.VerifyChallengeToken(reg, token, key)
|
||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||
// clear invalid cookie
|
||||
utils.ClearCookie(d.CookiePrefix+reg.Name, w, r)
|
||||
// clear invalid state
|
||||
d.ClearChallengeToken(reg)
|
||||
}
|
||||
|
||||
// prevent evaluating the challenge if not solved
|
||||
@@ -213,20 +271,46 @@ func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request)
|
||||
out, _, err := reg.Condition.Eval(d)
|
||||
// verify eligibility
|
||||
if err != nil {
|
||||
d.State.Logger(r).Error(err.Error(), "challenge", reg.Name)
|
||||
d.State.Logger(d.r).Error(err.Error(), "challenge", reg.Name)
|
||||
} else if out != nil && out.Type() == types.BoolType {
|
||||
if out.Equal(types.True) != types.True {
|
||||
// skip challenge match due to precondition!
|
||||
verifyResult = VerifyResultSkip
|
||||
continue
|
||||
return verifyResult, verifyState, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !verifyResult.Ok() && issuedChallenge == reg.Name {
|
||||
// we issued the challenge, must skip to prevent loops
|
||||
verifyResult = VerifyResultSkip
|
||||
if !verifyResult.Ok() && d.issuedChallenge == reg.Name {
|
||||
// we issued the challenge, must skip to prevent loops
|
||||
verifyResult = VerifyResultSkip
|
||||
}
|
||||
|
||||
return verifyResult, verifyState, err
|
||||
}
|
||||
|
||||
func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
challengeMap, err := d.verifyChallengeState()
|
||||
if err != nil {
|
||||
if !errors.Is(err, http.ErrNoCookie) {
|
||||
//clear invalid cookie and continue
|
||||
utils.ClearCookie(d.cookieName, w, r)
|
||||
}
|
||||
challengeMap = make(TokenChallengeMap)
|
||||
}
|
||||
d.ChallengeMap = challengeMap
|
||||
|
||||
for _, reg := range d.State.GetChallenges() {
|
||||
|
||||
key := GetChallengeKeyForRequest(d.State, reg, d.Expiration(reg.Duration), r)
|
||||
verifyResult, verifyState, err := d.verifyChallenge(reg, key)
|
||||
if err != nil {
|
||||
// clear invalid state
|
||||
d.ClearChallengeToken(reg)
|
||||
}
|
||||
|
||||
d.ChallengeVerify[reg.Id()] = verifyResult
|
||||
d.ChallengeState[reg.Id()] = verifyState
|
||||
}
|
||||
@@ -240,13 +324,22 @@ func (d *RequestData) HasValidChallenge(id Id) bool {
|
||||
return d.ChallengeVerify[id].Ok()
|
||||
}
|
||||
|
||||
func (d *RequestData) ResponseHeaders(headers http.Header) {
|
||||
func (d *RequestData) ResponseHeaders(w http.ResponseWriter) {
|
||||
// send these to client so we consistently get the headers
|
||||
//w.Header().Set("Accept-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
|
||||
//w.Header().Set("Critical-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
|
||||
|
||||
if d.State.Settings().MainName != "" {
|
||||
headers.Add("Via", fmt.Sprintf("%s %s@%s", d.r.Proto, d.State.Settings().MainName, d.State.Settings().MainVersion))
|
||||
w.Header().Add("Via", fmt.Sprintf("%s %s@%s", d.r.Proto, d.State.Settings().MainName, d.State.Settings().MainVersion))
|
||||
}
|
||||
|
||||
if d.challengeMapModified {
|
||||
expiration := d.Expiration(DefaultDuration)
|
||||
if token, err := d.issueChallengeState(expiration); err == nil {
|
||||
utils.SetCookie(d.cookieName, token, expiration, w, d.r)
|
||||
} else {
|
||||
d.State.Logger(d.r).Error("error while issuing cookie", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,3 +375,111 @@ func (d *RequestData) RequestHeaders(headers http.Header) {
|
||||
|
||||
maps.Copy(headers, d.ExtraHeaders)
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
State TokenChallengeMap `json:"state"`
|
||||
|
||||
Expiry jwt.NumericDate `json:"exp,omitempty"`
|
||||
NotBefore jwt.NumericDate `json:"nbf,omitempty"`
|
||||
IssuedAt jwt.NumericDate `json:"iat,omitempty"`
|
||||
}
|
||||
|
||||
type TokenChallengeMap map[string]TokenChallenge
|
||||
|
||||
type TokenChallenge struct {
|
||||
Key []byte `json:"key"`
|
||||
Result []byte `json:"result,omitempty"`
|
||||
Ok bool `json:"ok"`
|
||||
|
||||
Expiry jwt.NumericDate `json:"exp,omitempty"`
|
||||
NotBefore jwt.NumericDate `json:"nbf,omitempty"`
|
||||
IssuedAt jwt.NumericDate `json:"iat,omitempty"`
|
||||
}
|
||||
|
||||
func (d *RequestData) verifyChallengeState() (TokenChallengeMap, error) {
|
||||
cookie, err := d.r.Cookie(d.cookieName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cookie == nil {
|
||||
return nil, http.ErrNoCookie
|
||||
}
|
||||
encryptedToken, err := jwt.ParseSignedAndEncrypted(cookie.Value,
|
||||
[]jose.KeyAlgorithm{jose.DIRECT},
|
||||
[]jose.ContentEncryption{jose.A256GCM},
|
||||
[]jose.SignatureAlgorithm{jose.EdDSA},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signedToken, err := encryptedToken.Decrypt(d.cookieKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var i Token
|
||||
err = signedToken.Claims(d.State.PublicKey(), &i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if i.Expiry.Time().Compare(time.Now()) < 0 {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
if i.NotBefore.Time().Compare(time.Now()) > 0 {
|
||||
return nil, errors.New("token not valid yet")
|
||||
}
|
||||
|
||||
return i.State, nil
|
||||
}
|
||||
|
||||
func (d *RequestData) issueChallengeState(until time.Time) (string, error) {
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.EdDSA,
|
||||
Key: d.State.PrivateKey(),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
encrypter, err := jose.NewEncrypter(jose.A256GCM, jose.Recipient{
|
||||
Algorithm: jose.DIRECT,
|
||||
Key: d.cookieKey(),
|
||||
}, (&jose.EncrypterOptions{
|
||||
Compression: jose.DEFLATE,
|
||||
}).WithContentType("JWT"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return jwt.SignedAndEncrypted(signer, encrypter).Claims(Token{
|
||||
State: d.ChallengeMap,
|
||||
Expiry: jwt.NumericDate(until.Unix()),
|
||||
NotBefore: jwt.NumericDate(time.Now().UTC().AddDate(0, 0, -1).Unix()),
|
||||
IssuedAt: jwt.NumericDate(time.Now().UTC().Unix()),
|
||||
}).Serialize()
|
||||
}
|
||||
|
||||
func (d *RequestData) cookieKey() []byte {
|
||||
sum := sha256.New()
|
||||
sum.Write([]byte(d.r.Host))
|
||||
sum.Write([]byte{0})
|
||||
sum.Write(d.NetworkPrefix().AsSlice())
|
||||
sum.Write([]byte{0})
|
||||
sum.Write(d.State.PrivateKey())
|
||||
sum.Write([]byte{0})
|
||||
// version/compressor
|
||||
sum.Write([]byte("1.0/DEFLATE"))
|
||||
sum.Write([]byte{0})
|
||||
|
||||
return sum.Sum(nil)
|
||||
}
|
||||
|
||||
func (d *RequestData) cookieHostKey() []byte {
|
||||
sum := sha256.New()
|
||||
sum.Write([]byte(d.r.Host))
|
||||
sum.Write([]byte{0})
|
||||
sum.Write(d.NetworkPrefix().AsSlice())
|
||||
sum.Write([]byte{0})
|
||||
|
||||
return sum.Sum(nil)[:6]
|
||||
}
|
||||
|
@@ -125,18 +125,10 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
|
||||
}
|
||||
|
||||
if result.Bad() {
|
||||
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, nil, expiry, false)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
|
||||
data.IssueChallengeToken(reg, key, nil, expiry, false)
|
||||
return challenge.VerifyResultNotOK
|
||||
} else {
|
||||
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, nil, expiry, true)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
|
||||
data.IssueChallengeToken(reg, key, nil, expiry, true)
|
||||
return challenge.VerifyResultOK
|
||||
}
|
||||
}
|
||||
|
@@ -138,6 +138,8 @@ func VerifyHandlerChallengeResponseFunc(state StateInterface, data *RequestData,
|
||||
}
|
||||
reqUri.RawQuery = q.Encode()
|
||||
|
||||
data.ResponseHeaders(w)
|
||||
|
||||
http.Redirect(w, r, reqUri.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
@@ -147,6 +149,7 @@ func VerifyHandlerChallengeResponseFunc(state StateInterface, data *RequestData,
|
||||
state.ErrorPage(w, r, http.StatusForbidden, fmt.Errorf("access denied: failed challenge"), redirect)
|
||||
return
|
||||
}
|
||||
data.ResponseHeaders(w)
|
||||
http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
@@ -175,18 +178,12 @@ func VerifyHandlerFunc(state StateInterface, reg *Registration, verify VerifyFun
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !verifyResult.Ok() {
|
||||
utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
|
||||
state.ChallengeFailed(r, reg, nil, redirect, nil)
|
||||
responseFunc(state, data, w, r, verifyResult, nil, redirect)
|
||||
return nil
|
||||
}
|
||||
|
||||
challengeToken, err := reg.IssueChallengeToken(state.PrivateKey(), key, []byte(token), expiration, true)
|
||||
if err != nil {
|
||||
utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
|
||||
} else {
|
||||
utils.SetCookie(data.CookiePrefix+reg.Name, challengeToken, expiration, w, r)
|
||||
}
|
||||
data.IssueChallengeToken(reg, key, []byte(token), expiration, true)
|
||||
data.ChallengeVerify[reg.id] = verifyResult
|
||||
state.ChallengePassed(r, reg, redirect, nil)
|
||||
|
||||
@@ -194,7 +191,6 @@ func VerifyHandlerFunc(state StateInterface, reg *Registration, verify VerifyFun
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
|
||||
state.ChallengeFailed(r, reg, err, redirect, nil)
|
||||
responseFunc(state, data, w, r, VerifyResultFail, fmt.Errorf("access denied: error in challenge %s: %w", reg.Name, err), redirect)
|
||||
return
|
||||
|
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"git.gammaspectra.live/git/go-away/lib/challenge"
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"io"
|
||||
@@ -140,18 +139,10 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
|
||||
data := challenge.RequestDataFromContext(r.Context())
|
||||
|
||||
if response.StatusCode != params.HttpCode {
|
||||
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, false)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
|
||||
data.IssueChallengeToken(reg, key, sum, expiry, false)
|
||||
return challenge.VerifyResultNotOK
|
||||
} else {
|
||||
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, true)
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail
|
||||
}
|
||||
utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
|
||||
data.IssueChallengeToken(reg, key, sum, expiry, true)
|
||||
return challenge.VerifyResultOK
|
||||
}
|
||||
}
|
||||
|
@@ -66,7 +66,7 @@ func GetChallengeKeyForRequest(state StateInterface, reg *Registration, until ti
|
||||
hasher.Write([]byte{0})
|
||||
_ = binary.Write(hasher, binary.LittleEndian, until.UTC().Unix())
|
||||
hasher.Write([]byte{0})
|
||||
hasher.Write(state.PublicKey())
|
||||
hasher.Write(state.PrivateKeyFingerprint())
|
||||
hasher.Write([]byte{0})
|
||||
|
||||
sum := Key(hasher.Sum(nil))
|
||||
|
@@ -110,6 +110,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
|
||||
}
|
||||
|
||||
verifyResult, _ := verifier(key, []byte(token), r)
|
||||
|
||||
data.ResponseHeaders(w)
|
||||
|
||||
if !verifyResult.Ok() {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else {
|
||||
|
@@ -1,18 +1,12 @@
|
||||
package challenge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
http_cel "codeberg.org/gone/http-cel"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/git/go-away/lib/policy"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"github.com/google/cel-go/cel"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -145,104 +139,10 @@ type Registration struct {
|
||||
|
||||
type VerifyFunc func(key Key, token []byte, r *http.Request) (VerifyResult, error)
|
||||
|
||||
type Token struct {
|
||||
Name string `json:"name"`
|
||||
Key []byte `json:"key"`
|
||||
Result []byte `json:"result,omitempty"`
|
||||
Ok bool `json:"ok"`
|
||||
|
||||
Expiry jwt.NumericDate `json:"exp,omitempty"`
|
||||
NotBefore jwt.NumericDate `json:"nbf,omitempty"`
|
||||
IssuedAt jwt.NumericDate `json:"iat,omitempty"`
|
||||
}
|
||||
|
||||
func (reg Registration) Id() Id {
|
||||
return reg.id
|
||||
}
|
||||
|
||||
func (reg Registration) IssueChallengeToken(privateKey ed25519.PrivateKey, key Key, result []byte, until time.Time, ok bool) (token string, err error) {
|
||||
signer, err := jose.NewSigner(jose.SigningKey{
|
||||
Algorithm: jose.EdDSA,
|
||||
Key: privateKey,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err = jwt.Signed(signer).Claims(Token{
|
||||
Name: reg.Name,
|
||||
Key: key[:],
|
||||
Result: result,
|
||||
Ok: ok,
|
||||
Expiry: jwt.NumericDate(until.Unix()),
|
||||
NotBefore: jwt.NumericDate(time.Now().UTC().AddDate(0, 0, -1).Unix()),
|
||||
IssuedAt: jwt.NumericDate(time.Now().UTC().Unix()),
|
||||
}).Serialize()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
var ErrVerifyKeyMismatch = errors.New("verify: key mismatch")
|
||||
var ErrVerifyVerifyMismatch = errors.New("verify: verification mismatch")
|
||||
var ErrTokenExpired = errors.New("token: expired")
|
||||
|
||||
func (reg Registration) VerifyChallengeToken(publicKey ed25519.PublicKey, expectedKey Key, r *http.Request) (VerifyResult, VerifyState, error) {
|
||||
cookie, err := r.Cookie(RequestDataFromContext(r.Context()).CookiePrefix + reg.Name)
|
||||
if err != nil {
|
||||
return VerifyResultNone, VerifyStateNone, err
|
||||
}
|
||||
if cookie == nil {
|
||||
return VerifyResultNone, VerifyStateNone, http.ErrNoCookie
|
||||
}
|
||||
|
||||
token, err := jwt.ParseSigned(cookie.Value, []jose.SignatureAlgorithm{jose.EdDSA})
|
||||
if err != nil {
|
||||
return VerifyResultFail, VerifyStateNone, err
|
||||
}
|
||||
|
||||
var i Token
|
||||
err = token.Claims(publicKey, &i)
|
||||
if err != nil {
|
||||
return VerifyResultFail, VerifyStateNone, err
|
||||
}
|
||||
|
||||
if i.Name != reg.Name {
|
||||
return VerifyResultFail, VerifyStateNone, errors.New("token invalid name")
|
||||
}
|
||||
if i.Expiry.Time().Compare(time.Now()) < 0 {
|
||||
return VerifyResultFail, VerifyStateNone, ErrTokenExpired
|
||||
}
|
||||
if i.NotBefore.Time().Compare(time.Now()) > 0 {
|
||||
return VerifyResultFail, VerifyStateNone, errors.New("token not valid yet")
|
||||
}
|
||||
|
||||
if bytes.Compare(expectedKey[:], i.Key) != 0 {
|
||||
return VerifyResultFail, VerifyStateNone, ErrVerifyKeyMismatch
|
||||
}
|
||||
|
||||
if reg.Verify != nil {
|
||||
if rand.Float64() < reg.VerifyProbability {
|
||||
// random spot check
|
||||
if ok, err := reg.Verify(expectedKey, i.Result, r); err != nil {
|
||||
return VerifyResultFail, VerifyStateFull, err
|
||||
} else if ok == VerifyResultNotOK {
|
||||
return VerifyResultNotOK, VerifyStateFull, nil
|
||||
} else if !ok.Ok() {
|
||||
return ok, VerifyStateFull, ErrVerifyVerifyMismatch
|
||||
} else {
|
||||
return ok, VerifyStateFull, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !i.Ok {
|
||||
return VerifyResultNotOK, VerifyStateBrief, nil
|
||||
}
|
||||
return VerifyResultOK, VerifyStateBrief, nil
|
||||
}
|
||||
|
||||
type FillRegistration func(state StateInterface, reg *Registration, parameters ast.Node) error
|
||||
|
||||
var Runtimes = make(map[string]FillRegistration)
|
||||
|
@@ -45,6 +45,9 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
|
||||
//TODO: add other types inside css that need to be loaded!
|
||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||
w.Header().Set("Content-Length", "0")
|
||||
|
||||
data.ResponseHeaders(w)
|
||||
|
||||
if !verifyResult.Ok() {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
} else {
|
||||
|
@@ -23,7 +23,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
|
||||
//TODO: log
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data.ResponseHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
err = scriptTemplate.Execute(w, map[string]any{
|
||||
|
@@ -89,6 +89,7 @@ type StateInterface interface {
|
||||
RegisterCondition(operator string, conditions ...string) (cel.Program, error)
|
||||
|
||||
Client() *http.Client
|
||||
PrivateKeyFingerprint() []byte
|
||||
PrivateKey() ed25519.PrivateKey
|
||||
PublicKey() ed25519.PublicKey
|
||||
|
||||
|
@@ -267,10 +267,13 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if !strings.HasPrefix(c.Name, utils.CookiePrefix) {
|
||||
if !strings.HasPrefix(c.Name, utils.DefaultCookiePrefix) {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
|
||||
// set response headers
|
||||
data.ResponseHeaders(w)
|
||||
}
|
||||
|
||||
for _, rule := range state.rules {
|
||||
@@ -323,7 +326,5 @@ func (state *State) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data.EvaluateChallenges(w, r)
|
||||
|
||||
data.ResponseHeaders(w.Header())
|
||||
|
||||
state.Mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
@@ -26,6 +26,10 @@ func (state *State) PrivateKey() ed25519.PrivateKey {
|
||||
return state.privateKey
|
||||
}
|
||||
|
||||
func (state *State) PrivateKeyFingerprint() []byte {
|
||||
return state.privateKeyFingerprint
|
||||
}
|
||||
|
||||
func (state *State) PublicKey() ed25519.PublicKey {
|
||||
return state.publicKey
|
||||
}
|
||||
|
@@ -29,7 +29,6 @@ type RuleState struct {
|
||||
}
|
||||
|
||||
func NewRuleState(state challenge.StateInterface, r policy.Rule, replacer *strings.Replacer, parent *RuleState) (RuleState, error) {
|
||||
fp := sha256.Sum256(state.PrivateKey())
|
||||
hasher := sha256.New()
|
||||
if parent != nil {
|
||||
hasher.Write([]byte(parent.Name))
|
||||
@@ -38,7 +37,7 @@ func NewRuleState(state challenge.StateInterface, r policy.Rule, replacer *strin
|
||||
}
|
||||
hasher.Write([]byte(r.Name))
|
||||
hasher.Write([]byte{0})
|
||||
hasher.Write(fp[:])
|
||||
hasher.Write(state.PrivateKeyFingerprint())
|
||||
sum := hasher.Sum(nil)
|
||||
|
||||
rule := RuleState{
|
||||
|
@@ -35,8 +35,9 @@ type State struct {
|
||||
|
||||
programEnv *cel.Env
|
||||
|
||||
publicKey ed25519.PublicKey
|
||||
privateKey ed25519.PrivateKey
|
||||
publicKey ed25519.PublicKey
|
||||
privateKey ed25519.PrivateKey
|
||||
privateKeyFingerprint []byte
|
||||
|
||||
opt settings.Settings
|
||||
settings policy.StateSettings
|
||||
@@ -101,6 +102,9 @@ func NewState(p policy.Policy, opt settings.Settings, settings policy.StateSetti
|
||||
}
|
||||
}
|
||||
|
||||
fp := sha256.Sum256(state.privateKey)
|
||||
state.privateKeyFingerprint = fp[:]
|
||||
|
||||
if templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"] == nil {
|
||||
|
||||
if data, err := os.ReadFile(state.opt.ChallengeTemplate); err == nil && len(data) > 0 {
|
||||
|
@@ -107,6 +107,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
|
||||
if err != nil {
|
||||
state.ErrorPage(w, r, http.StatusInternalServerError, err, "")
|
||||
} else {
|
||||
data.ResponseHeaders(w)
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
@@ -141,6 +142,7 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int
|
||||
// nested errors!
|
||||
panic(err2)
|
||||
} else {
|
||||
data.ResponseHeaders(w)
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
|
Reference in New Issue
Block a user