more mschap v2, start peap extension type 33

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2025-05-24 01:25:09 +02:00
parent e0c837257c
commit fad18db70b
8 changed files with 275 additions and 5 deletions

View File

@ -44,7 +44,7 @@ func (p *Payload) Decode(raw []byte) error {
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
p.MsgType = protocol.Type(raw[4])
}
log.WithField("raw", debug.FormatBytes(raw)).WithField("payload", fmt.Sprintf("%T", p.Payload)).Trace("EAP: decode raw")
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
p.RawPayload = raw[5:]
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
if err != nil {

View File

@ -0,0 +1,26 @@
package mschapv2
import (
"bytes"
"errors"
"layeh.com/radius/rfc2759"
)
func (p *Payload) checkChapPassword(res *Response) ([]byte, error) {
byteUser := []byte("foo")
bytePwd := []byte("bar")
ntResponse, err := rfc2759.GenerateNTResponse(p.st.Challenge, p.st.PeerChallenge, byteUser, bytePwd)
if err != nil {
return nil, err
}
if !bytes.Equal(ntResponse, res.NTResponse) {
return nil, errors.New("nt response mismatch")
}
authenticatorResponse, err := rfc2759.GenerateAuthenticatorResponse(p.st.Challenge, p.st.PeerChallenge, ntResponse, byteUser, bytePwd)
if err != nil {
return nil, err
}
return []byte(authenticatorResponse), nil
}

View File

@ -0,0 +1,23 @@
package mschapv2
import (
"bytes"
"errors"
)
type Response struct {
Challenge []byte
NTResponse []byte
Flags uint8
}
func ParseResponse(raw []byte) (*Response, error) {
res := &Response{}
res.Challenge = raw[:challengeValueSize]
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
}
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
return res, nil
}

View File

@ -0,0 +1,23 @@
package mschapv2
import "encoding/binary"
type SuccessRequest struct {
*Payload
Authenticator []byte
}
// A success request is encoded slightly differently, it doesn't have a challenge and as such
// doesn't need to encode the length of it
func (sr *SuccessRequest) Encode() ([]byte, error) {
encoded := []byte{
byte(sr.OpCode),
sr.MSCHAPv2ID,
0,
0,
}
encoded = append(encoded, sr.Authenticator...)
sr.MSLength = uint16(len(encoded))
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
return encoded, nil
}

View File

@ -1,7 +1,15 @@
package mschapv2
import (
"encoding/binary"
"fmt"
"github.com/gorilla/securecookie"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/radius/eap/debug"
"goauthentik.io/internal/outpost/radius/eap/protocol"
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
)
const TypeMSCHAPv2 protocol.Type = 26
@ -10,7 +18,33 @@ func Protocol() protocol.Payload {
return &Payload{}
}
const (
challengeValueSize = 16
responseValueSize = 49
responseReservedSize = 8
responseNTResponseSize = 24
)
type OpCode uint8
const (
OpChallenge OpCode = 1
OpResponse OpCode = 2
OpSuccess OpCode = 3
)
type Payload struct {
OpCode OpCode
MSCHAPv2ID uint8
MSLength uint16
ValueSize uint8
Challenge []byte
Response []byte
Name []byte
st *State
}
func (p *Payload) Type() protocol.Type {
@ -18,18 +52,100 @@ func (p *Payload) Type() protocol.Type {
}
func (p *Payload) Decode(raw []byte) error {
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
p.OpCode = OpCode(raw[0])
if p.OpCode == OpSuccess {
return nil
}
// TODO: Validate against root EAP packet
p.MSCHAPv2ID = raw[1]
p.MSLength = binary.BigEndian.Uint16(raw[2:])
p.ValueSize = raw[4]
if p.ValueSize != responseValueSize {
return fmt.Errorf("mschapv2: incorrect value size: %d", p.ValueSize)
}
p.Response = raw[5 : p.ValueSize+5]
p.Name = raw[5+p.ValueSize:]
if int(p.MSLength) != len(raw) {
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
}
return nil
}
func (p *Payload) Encode() ([]byte, error) {
return []byte{}, nil
encoded := []byte{
byte(p.OpCode),
p.MSCHAPv2ID,
0,
0,
byte(len(p.Challenge)),
}
encoded = append(encoded, p.Challenge...)
encoded = append(encoded, p.Name...)
p.MSLength = uint16(len(encoded))
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
return encoded, nil
}
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
defer func() {
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
}()
rootEap := ctx.RootPayload().(*eap.Payload)
if ctx.IsProtocolStart(TypeMSCHAPv2) {
ctx.EndInnerProtocol(protocol.StatusError, nil)
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
p.st = &State{
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
}
return &Payload{
OpCode: OpChallenge,
MSCHAPv2ID: rootEap.ID + 1,
Challenge: p.st.Challenge,
Name: []byte("authentik"),
}
}
return nil
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
response := &Payload{
MSCHAPv2ID: rootEap.ID + 1,
}
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
if p.OpCode == OpResponse {
res, err := ParseResponse(p.Response)
if err != nil {
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
return nil
}
p.st.PeerChallenge = res.Challenge
auth, err := p.checkChapPassword(res)
if err != nil {
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
return nil
}
ctx.Log().Info("MSCHAPv2: Successfully checked password")
succ := &SuccessRequest{
Payload: &Payload{
OpCode: OpSuccess,
},
Authenticator: auth,
}
return succ
} else if p.OpCode == OpSuccess {
return &peap.ExtensionPayload{
AVPs: []peap.ExtensionAVP{
{
Mandatory: true,
Type: peap.AVPAckResult,
Value: []byte{0, 1},
},
},
}
}
return response
}
func (p *Payload) Offerable() bool {
@ -37,5 +153,9 @@ func (p *Payload) Offerable() bool {
}
func (p *Payload) String() string {
return "<MSCHAPv2 Packet >"
return fmt.Sprintf(
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
p.OpCode,
p.MSCHAPv2ID,
)
}

View File

@ -0,0 +1,6 @@
package mschapv2
type State struct {
Challenge []byte
PeerChallenge []byte
}

View File

@ -0,0 +1,37 @@
package peap
import (
"errors"
"goauthentik.io/internal/outpost/radius/eap/protocol"
)
const TypePEAPExtension protocol.Type = 33
type ExtensionPayload struct {
AVPs []ExtensionAVP
}
func (ep *ExtensionPayload) Decode(raw []byte) error {
return errors.New("PEAP: Extension Payload does not support decoding")
}
func (ep *ExtensionPayload) Encode() ([]byte, error) {
return []byte{}, nil
}
func (ep *ExtensionPayload) Handle(protocol.Context) protocol.Payload {
return nil
}
func (ep *ExtensionPayload) Offerable() bool {
return false
}
func (ep *ExtensionPayload) String() string {
return "<PEAP Extension Payload>"
}
func (ep *ExtensionPayload) Type() protocol.Type {
return TypePEAPExtension
}

View File

@ -0,0 +1,35 @@
package peap
import "encoding/binary"
type AVPType uint16
const (
AVPAckResult AVPType = 3
)
type ExtensionAVP struct {
Mandatory bool
Type AVPType // 14-bit field
Length uint16
Value []byte
}
func (eavp ExtensionAVP) Encode() []byte {
buff := []byte{
0,
0,
0,
0,
}
t := uint16(eavp.Type)
// Type is a 14-bit number, the highest bit is the mandatory flag
if eavp.Mandatory {
t = t | 0b1000000000000000
}
// The next bit is reserved and should always be set to 0
t = t & 0b1011111111111111
binary.BigEndian.AppendUint16(buff, t)
binary.BigEndian.AppendUint16(buff[2:], uint16(len(eavp.Value)))
return append(buff, eavp.Value...)
}