diff --git a/internal/outpost/radius/eap/protocol/eap/payload.go b/internal/outpost/radius/eap/protocol/eap/payload.go index 54f692536a..702c73334d 100644 --- a/internal/outpost/radius/eap/protocol/eap/payload.go +++ b/internal/outpost/radius/eap/protocol/eap/payload.go @@ -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 { diff --git a/internal/outpost/radius/eap/protocol/mschapv2/auth.go b/internal/outpost/radius/eap/protocol/mschapv2/auth.go new file mode 100644 index 0000000000..2a14bd1743 --- /dev/null +++ b/internal/outpost/radius/eap/protocol/mschapv2/auth.go @@ -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 +} diff --git a/internal/outpost/radius/eap/protocol/mschapv2/op_response.go b/internal/outpost/radius/eap/protocol/mschapv2/op_response.go new file mode 100644 index 0000000000..b0aeae9663 --- /dev/null +++ b/internal/outpost/radius/eap/protocol/mschapv2/op_response.go @@ -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 +} diff --git a/internal/outpost/radius/eap/protocol/mschapv2/op_success.go b/internal/outpost/radius/eap/protocol/mschapv2/op_success.go new file mode 100644 index 0000000000..07388b110c --- /dev/null +++ b/internal/outpost/radius/eap/protocol/mschapv2/op_success.go @@ -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 +} diff --git a/internal/outpost/radius/eap/protocol/mschapv2/payload.go b/internal/outpost/radius/eap/protocol/mschapv2/payload.go index b6374cf3a1..703c180a81 100644 --- a/internal/outpost/radius/eap/protocol/mschapv2/payload.go +++ b/internal/outpost/radius/eap/protocol/mschapv2/payload.go @@ -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 "" + return fmt.Sprintf( + "", + p.OpCode, + p.MSCHAPv2ID, + ) } diff --git a/internal/outpost/radius/eap/protocol/mschapv2/state.go b/internal/outpost/radius/eap/protocol/mschapv2/state.go new file mode 100644 index 0000000000..021095aa6f --- /dev/null +++ b/internal/outpost/radius/eap/protocol/mschapv2/state.go @@ -0,0 +1,6 @@ +package mschapv2 + +type State struct { + Challenge []byte + PeerChallenge []byte +} diff --git a/internal/outpost/radius/eap/protocol/peap/extension.go b/internal/outpost/radius/eap/protocol/peap/extension.go new file mode 100644 index 0000000000..85bca6d092 --- /dev/null +++ b/internal/outpost/radius/eap/protocol/peap/extension.go @@ -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 "" +} + +func (ep *ExtensionPayload) Type() protocol.Type { + return TypePEAPExtension +} diff --git a/internal/outpost/radius/eap/protocol/peap/extension_avp.go b/internal/outpost/radius/eap/protocol/peap/extension_avp.go new file mode 100644 index 0000000000..3c5ba1944b --- /dev/null +++ b/internal/outpost/radius/eap/protocol/peap/extension_avp.go @@ -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...) +}