1<?php
2
3/**
4 * This file is part of the FreeDSx LDAP package.
5 *
6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace FreeDSx\Ldap\Protocol\ClientProtocolHandler;
13
14use FreeDSx\Asn1\Exception\EncoderException;
15use FreeDSx\Ldap\Control\Control;
16use FreeDSx\Ldap\Exception\BindException;
17use FreeDSx\Ldap\Exception\ConnectionException;
18use FreeDSx\Ldap\Exception\OperationException;
19use FreeDSx\Ldap\Exception\ProtocolException;
20use FreeDSx\Ldap\Exception\ReferralException;
21use FreeDSx\Ldap\Exception\UnsolicitedNotificationException;
22use FreeDSx\Ldap\Operation\Request\SaslBindRequest;
23use FreeDSx\Ldap\Operation\Response\BindResponse;
24use FreeDSx\Ldap\Operation\ResultCode;
25use FreeDSx\Ldap\Operations;
26use FreeDSx\Ldap\Protocol\LdapMessageResponse;
27use FreeDSx\Ldap\Protocol\Queue\ClientQueue;
28use FreeDSx\Ldap\Protocol\Queue\MessageWrapper\SaslMessageWrapper;
29use FreeDSx\Sasl\Exception\SaslException;
30use FreeDSx\Sasl\Mechanism\MechanismInterface;
31use FreeDSx\Sasl\Sasl;
32use FreeDSx\Sasl\SaslContext;
33
34/**
35 * Logic for handling a SASL bind.
36 *
37 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
38 */
39class ClientSaslBindHandler implements RequestHandlerInterface
40{
41    use MessageCreationTrait;
42
43    /**
44     * @var Control[]
45     */
46    protected $controls;
47
48    /**
49     * @var Sasl
50     */
51    protected $sasl;
52
53    public function __construct(?Sasl $sasl = null)
54    {
55        $this->sasl = $sasl ?? new Sasl();
56    }
57
58    /**
59     * {@@inheritDoc}
60     * @param ClientProtocolContext $context
61     * @return LdapMessageResponse|null
62     * @throws BindException
63     * @throws ProtocolException
64     * @throws EncoderException
65     * @throws ConnectionException
66     * @throws OperationException
67     * @throws ReferralException
68     * @throws UnsolicitedNotificationException
69     * @throws SaslException
70     * @throws \FreeDSx\Socket\Exception\ConnectionException
71     */
72    public function handleRequest(ClientProtocolContext $context): ?LdapMessageResponse
73    {
74        /** @var SaslBindRequest $request */
75        $request = $context->getRequest();
76        $this->controls = $context->getControls();
77
78        # If we are selecting a mechanism from the RootDSE, we must check for a downgrade afterwards.
79        $detectDowngrade = ($request->getMechanism() === '');
80        $mech = $this->selectSaslMech($request, $context);
81
82        $queue = $context->getQueue();
83        $message = $context->messageToSend();
84        $queue->sendMessage($message);
85
86        /** @var LdapMessageResponse $response */
87        $response = $queue->getMessage($message->getMessageId());
88        $saslResponse = $response->getResponse();
89        if (!$saslResponse instanceof BindResponse) {
90            throw new ProtocolException(sprintf(
91                'Expected a bind response during a SASL bind. But got: %s',
92                get_class($saslResponse)
93            ));
94        }
95        if ($saslResponse->getResultCode() !== ResultCode::SASL_BIND_IN_PROGRESS) {
96            return $response;
97        }
98        $response = $this->processSaslChallenge($request, $queue, $saslResponse, $mech);
99        if (
100            $detectDowngrade
101            && $response !== null
102            && $response->getResponse() instanceof BindResponse
103            && $response->getResponse()->getResultCode() === ResultCode::SUCCESS
104        ) {
105            $this->checkDowngradeAttempt($context);
106        }
107
108        return $response;
109    }
110
111    /**
112     * @param SaslBindRequest $request
113     * @param ClientProtocolContext $context
114     * @return MechanismInterface
115     * @throws BindException
116     * @throws ProtocolException
117     * @throws EncoderException
118     * @throws ConnectionException
119     * @throws OperationException
120     * @throws ReferralException
121     * @throws UnsolicitedNotificationException
122     * @throws SaslException
123     * @throws \FreeDSx\Socket\Exception\ConnectionException
124     */
125    protected function selectSaslMech(SaslBindRequest $request, ClientProtocolContext $context): MechanismInterface
126    {
127        if ($request->getMechanism() !== '') {
128            $mech = $this->sasl->get($request->getMechanism());
129            $request->setMechanism($mech->getName());
130
131            return $mech;
132        }
133        $rootDse = $context->getRootDse();
134        $availableMechs = $rootDse->get('supportedSaslMechanisms');
135        $availableMechs = $availableMechs === null ? [] : $availableMechs->getValues();
136        $mech = $this->sasl->select($availableMechs, $request->getOptions());
137        $request->setMechanism($mech->getName());
138
139        return $mech;
140    }
141
142    /**
143     * @param SaslBindRequest $request
144     * @param ClientQueue $queue
145     * @param BindResponse $saslResponse
146     * @param MechanismInterface $mech
147     * @return LdapMessageResponse
148     * @throws BindException
149     * @throws ProtocolException
150     * @throws EncoderException
151     * @throws UnsolicitedNotificationException
152     * @throws SaslException
153     * @throws \FreeDSx\Socket\Exception\ConnectionException
154     */
155    protected function processSaslChallenge(
156        SaslBindRequest $request,
157        ClientQueue $queue,
158        BindResponse $saslResponse,
159        MechanismInterface $mech
160    ): ?LdapMessageResponse {
161        $challenge = $mech->challenge();
162        $response = null;
163
164        do {
165            $context = $challenge->challenge($saslResponse->getSaslCredentials(), $request->getOptions());
166            $saslBind = Operations::bindSasl($request->getOptions(), $request->getMechanism(), $context->getResponse());
167            $response = $this->sendRequestGetResponse($saslBind, $queue);
168            $saslResponse = $response->getResponse();
169            if (!$saslResponse instanceof BindResponse) {
170                throw new BindException(sprintf(
171                    'Expected a bind response during a SASL bind. But got: %s',
172                    get_class($saslResponse)
173                ));
174            }
175        } while (!$this->isChallengeComplete($context, $saslResponse));
176
177        if (!$context->isComplete()) {
178            $context = $challenge->challenge($saslResponse->getSaslCredentials(), $request->getOptions());
179        }
180
181        if ($saslResponse->getResultCode() === ResultCode::SUCCESS && $context->hasSecurityLayer()) {
182            $queue->setMessageWrapper(new SaslMessageWrapper($mech->securityLayer(), $context));
183        }
184
185        return $response;
186    }
187
188    /**
189     * @param SaslBindRequest $request
190     * @param ClientQueue $queue
191     * @return LdapMessageResponse
192     * @throws ProtocolException
193     * @throws EncoderException
194     * @throws UnsolicitedNotificationException
195     * @throws \FreeDSx\Socket\Exception\ConnectionException
196     */
197    protected function sendRequestGetResponse(SaslBindRequest $request, ClientQueue $queue): LdapMessageResponse
198    {
199        $messageTo = $this->makeRequest($queue, $request, $this->controls);
200        $queue->sendMessage($messageTo);
201
202        /** @var LdapMessageResponse $messageFrom */
203        $messageFrom = $queue->getMessage($messageTo->getMessageId());
204
205        return $messageFrom;
206    }
207
208    protected function isChallengeComplete(SaslContext $context, BindResponse $response): bool
209    {
210        if ($context->isComplete() || $context->getResponse() === null) {
211            return true;
212        }
213
214        if ($response->getResultCode() === ResultCode::SUCCESS) {
215            return true;
216        }
217
218        return $response->getResultCode() !== ResultCode::SASL_BIND_IN_PROGRESS;
219    }
220
221    /**
222     * @param ClientProtocolContext $context
223     * @throws BindException
224     * @throws ProtocolException
225     * @throws EncoderException
226     * @throws ConnectionException
227     * @throws OperationException
228     * @throws ReferralException
229     * @throws UnsolicitedNotificationException
230     * @throws SaslException
231     * @throws \FreeDSx\Socket\Exception\ConnectionException
232     */
233    protected function checkDowngradeAttempt(ClientProtocolContext $context): void
234    {
235        $priorRootDse = $context->getRootDse();
236        $rootDse = $context->getRootDse(true);
237
238        $mechs = $rootDse->get('supportedSaslMechanisms');
239        $priorMechs = $priorRootDse->get('supportedSaslMechanisms');
240        $priorMechs = $priorMechs !== null ? $priorMechs->getValues() : [];
241        $mechs = $mechs !== null ? $mechs->getValues() : [];
242
243        if (count(array_diff($mechs, $priorMechs)) !== 0) {
244            throw new BindException(
245                'Possible SASL downgrade attack detected. The advertised SASL mechanisms have changed.'
246            );
247        }
248    }
249}
250