diff --git a/lib/action/block.go b/lib/action/block.go index ed12397..bfcc7d9 100644 --- a/lib/action/block.go +++ b/lib/action/block.go @@ -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())) diff --git a/lib/action/code.go b/lib/action/code.go index 7abf89f..40f04f3 100644 --- a/lib/action/code.go +++ b/lib/action/code.go @@ -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 } diff --git a/lib/challenge/cookie/cookie.go b/lib/challenge/cookie/cookie.go index daa5933..2a97675 100644 --- a/lib/challenge/cookie/cookie.go +++ b/lib/challenge/cookie/cookie.go @@ -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 } diff --git a/lib/challenge/data.go b/lib/challenge/data.go index b327655..a10051a 100644 --- a/lib/challenge/data.go +++ b/lib/challenge/data.go @@ -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] +} diff --git a/lib/challenge/dnsbl/dnsbl.go b/lib/challenge/dnsbl/dnsbl.go index 19a1465..cacd2a4 100644 --- a/lib/challenge/dnsbl/dnsbl.go +++ b/lib/challenge/dnsbl/dnsbl.go @@ -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 } } diff --git a/lib/challenge/helper.go b/lib/challenge/helper.go index 51dc58e..9c5b5e5 100644 --- a/lib/challenge/helper.go +++ b/lib/challenge/helper.go @@ -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 diff --git a/lib/challenge/http/http.go b/lib/challenge/http/http.go index b54a4b6..802f8ea 100644 --- a/lib/challenge/http/http.go +++ b/lib/challenge/http/http.go @@ -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 } } diff --git a/lib/challenge/key.go b/lib/challenge/key.go index 7b15932..6574246 100644 --- a/lib/challenge/key.go +++ b/lib/challenge/key.go @@ -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)) diff --git a/lib/challenge/preload-link/preload-link.go b/lib/challenge/preload-link/preload-link.go index 64ca78c..1acac99 100644 --- a/lib/challenge/preload-link/preload-link.go +++ b/lib/challenge/preload-link/preload-link.go @@ -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 { diff --git a/lib/challenge/register.go b/lib/challenge/register.go index 353206b..25f8ad1 100644 --- a/lib/challenge/register.go +++ b/lib/challenge/register.go @@ -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) diff --git a/lib/challenge/resource-load/resource-load.go b/lib/challenge/resource-load/resource-load.go index 4c3e232..8ffc017 100644 --- a/lib/challenge/resource-load/resource-load.go +++ b/lib/challenge/resource-load/resource-load.go @@ -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 { diff --git a/lib/challenge/script.go b/lib/challenge/script.go index 459ec4f..7abf11f 100644 --- a/lib/challenge/script.go +++ b/lib/challenge/script.go @@ -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{ diff --git a/lib/challenge/types.go b/lib/challenge/types.go index 51a3a75..812f117 100644 --- a/lib/challenge/types.go +++ b/lib/challenge/types.go @@ -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 diff --git a/lib/http.go b/lib/http.go index 13fdc0d..8c995f1 100644 --- a/lib/http.go +++ b/lib/http.go @@ -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) } diff --git a/lib/interface.go b/lib/interface.go index f91377b..c350ee7 100644 --- a/lib/interface.go +++ b/lib/interface.go @@ -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 } diff --git a/lib/rule.go b/lib/rule.go index 88c37b7..cef8383 100644 --- a/lib/rule.go +++ b/lib/rule.go @@ -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{ diff --git a/lib/state.go b/lib/state.go index 2d4f584..97f081e 100644 --- a/lib/state.go +++ b/lib/state.go @@ -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 { diff --git a/lib/template.go b/lib/template.go index dc73c2c..aeaced0 100644 --- a/lib/template.go +++ b/lib/template.go @@ -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()) } diff --git a/utils/cookie.go b/utils/cookie.go index 48587a2..89de6e1 100644 --- a/utils/cookie.go +++ b/utils/cookie.go @@ -6,7 +6,7 @@ import ( "time" ) -var CookiePrefix = ".go-away-" +var DefaultCookiePrefix = ".go-away-" // getValidHost Gets a valid host for an http.Cookie Domain field // TODO: bug: does not work with IPv6, see https://github.com/golang/go/issues/65521