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