Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/extension-installer": "^1.4",
"symplify/easy-coding-standard": "^9.4",
"symplify/easy-coding-standard": "^13",
"squizlabs/php_codesniffer": "^3.7",
"slevomat/coding-standard": "^7.2"
},
Expand Down
31 changes: 13 additions & 18 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,21 @@
use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer;
use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\CodingStandard\Fixer\Strict\BlankLineAfterStrictTypesFixer;
use Symplify\EasyCodingStandard\ValueObject\Option;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
use Symplify\EasyCodingStandard\Config\ECSConfig;

return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set(Option::PATHS, [
return ECSConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPreparedSets(psr12: true)
->withConfiguredRule(
ArraySyntaxFixer::class,
['syntax' => 'short'],
)
->withRules([
BlankLineAfterStrictTypesFixer::class,
BlankLineAfterOpeningTagFixer::class,
NoUnusedImportsFixer::class,
]);

$services = $containerConfigurator->services();
$services->set(ArraySyntaxFixer::class)
->call('configure', [[
'syntax' => 'short',
]]);
$services->set(BlankLineAfterStrictTypesFixer::class);
$services->set(BlankLineAfterOpeningTagFixer::class);
$services->set(NoUnusedImportsFixer::class);

$containerConfigurator->import(SetList::PSR_12);
};
12 changes: 6 additions & 6 deletions src/FreeDSx/Sasl/Challenge/DigestMD5Challenge.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,12 @@ private function generateServerChallenge(DigestMD5Options $options): Message
DigestMD5MessageType::SERVER_CHALLENGE,
[
'use_integrity' => $options->isUseIntegrity(),
'use_privacy' => $options->isUsePrivacy(),
'nonce' => $options->getNonce(),
'nonce_size' => $options->getNonceSize(),
'realm' => $options->getRealm(),
'maxbuf' => $options->getMaxbuf(),
'cipher' => $options->getCipher(),
'use_privacy' => $options->isUsePrivacy(),
'nonce' => $options->getNonce(),
'nonce_size' => $options->getNonceSize(),
'realm' => $options->getRealm(),
'maxbuf' => $options->getMaxbuf(),
'cipher' => $options->getCipher(),
],
);

Expand Down
97 changes: 97 additions & 0 deletions src/FreeDSx/Sasl/Challenge/ExternalChallenge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx SASL package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Sasl\Challenge;

use FreeDSx\Sasl\Encoder\ExternalEncoder;
use FreeDSx\Sasl\Exception\SaslException;
use FreeDSx\Sasl\Message;
use FreeDSx\Sasl\Options\ChallengeOptionsInterface;
use FreeDSx\Sasl\Options\ExternalOptions;
use FreeDSx\Sasl\SaslContext;

/**
* The EXTERNAL challenge / response class.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class ExternalChallenge implements ChallengeInterface
{
use ResolvesOptionsTrait;

private ExternalEncoder $encoder;

private SaslContext $context;

public function __construct(bool $isServerMode = false)
{
$this->encoder = new ExternalEncoder();
$this->context = new SaslContext();
$this->context->setIsServerMode($isServerMode);
}

public function challenge(
?string $received = null,
?ChallengeOptionsInterface $options = null,
): SaslContext {
$resolved = $this->resolveOptions(
$options ?? new ExternalOptions(),
ExternalOptions::class,
);

return $this->context->isServerMode()
? $this->serverProcess($received, $resolved)
: $this->clientProcess($resolved);
}

/**
* @throws SaslException
*/
private function serverProcess(
?string $received,
ExternalOptions $options,
): SaslContext {
$validate = $options->getValidate();
if ($validate === null) {
throw new SaslException('You must pass a callable validate option to the external mechanism in server mode.');
}

// The credential is the verified peer identity from a lower layer; the payload is only the optional authzId.
$rawAuthzId = $this->encoder->decode($received ?? '', $this->context)->get('authzid');
$authzId = is_string($rawAuthzId)
? $rawAuthzId
: null;

$this->context->setIsComplete(true);
$this->context->setIsAuthenticated($validate($authzId));

return $this->context;
}

private function clientProcess(ExternalOptions $options): SaslContext
{
$data = [];
if ($options->getAuthzId() !== null) {
$data['authzid'] = $options->getAuthzId();
}

$this->context->setResponse($this->encoder->encode(
new Message($data),
$this->context,
));
$this->context->setIsComplete(true);
$this->context->setIsAuthenticated(true);

return $this->context;
}
}
20 changes: 10 additions & 10 deletions src/FreeDSx/Sasl/Challenge/ScramChallenge.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@
* Values follow RFC 5802 (SHA-1: 4096) and RFC 7677 (SHA-256: 4096).
*/
private const MIN_ITERATIONS = [
'sha1' => 4096,
'sha224' => 4096,
'sha256' => 4096,
'sha384' => 4096,
'sha512' => 4096,
'sha1' => 4096,
'sha224' => 4096,
'sha256' => 4096,
'sha384' => 4096,
'sha512' => 4096,
'sha3-512' => 4096,
];

Expand All @@ -75,11 +75,11 @@
* for stronger hash variants.
*/
private const DEFAULT_ITERATIONS = [
'sha1' => 10000,
'sha224' => 15000,
'sha256' => 15000,
'sha384' => 10000,
'sha512' => 10000,
'sha1' => 10000,
'sha224' => 15000,
'sha256' => 15000,
'sha384' => 10000,
'sha512' => 10000,
'sha3-512' => 10000,
];

Expand Down
47 changes: 47 additions & 0 deletions src/FreeDSx/Sasl/Encoder/ExternalEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx SASL package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Sasl\Encoder;

use FreeDSx\Sasl\Message;
use FreeDSx\Sasl\SaslContext;

/**
* Encodes / decodes EXTERNAL messages. The payload is the optional authzId string (RFC 4422 §3.1).
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class ExternalEncoder implements EncoderInterface
{
public function encode(
Message $message,
SaslContext $context,
): string {
return $message->has('authzid')
? $message->getString('authzid')
: '';
}

public function decode(
string $data,
SaslContext $context,
): Message {
$message = new Message();

if ($data !== '') {
$message->set('authzid', $data);
}

return $message;
}
}
54 changes: 54 additions & 0 deletions src/FreeDSx/Sasl/Mechanism/ExternalMechanism.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx SASL package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Sasl\Mechanism;

use FreeDSx\Sasl\Challenge\ChallengeInterface;
use FreeDSx\Sasl\Challenge\ExternalChallenge;
use FreeDSx\Sasl\Exception\SaslException;
use FreeDSx\Sasl\Security\SecurityLayerInterface;
use FreeDSx\Sasl\SecurityStrength;

/**
* The EXTERNAL mechanism.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final readonly class ExternalMechanism implements MechanismInterface
{
public function getName(): MechanismName
{
return MechanismName::EXTERNAL;
}

public function challenge(bool $serverMode = false): ChallengeInterface
{
return new ExternalChallenge($serverMode);
}

public function securityStrength(): SecurityStrength
{
return new SecurityStrength(
supportsIntegrity: false,
supportsPrivacy: false,
supportsAuth: true,
isPlainTextAuth: false,
maxKeySize: 0,
);
}

public function securityLayer(): SecurityLayerInterface
{
throw new SaslException('The EXTERNAL mechanism does not support a security layer.');
}
}
1 change: 1 addition & 0 deletions src/FreeDSx/Sasl/Mechanism/MechanismName.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
enum MechanismName: string
{
case ANONYMOUS = 'ANONYMOUS';
case EXTERNAL = 'EXTERNAL';
case PLAIN = 'PLAIN';
case CRAM_MD5 = 'CRAM-MD5';
case DIGEST_MD5 = 'DIGEST-MD5';
Expand Down
64 changes: 64 additions & 0 deletions src/FreeDSx/Sasl/Options/ExternalOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx SASL package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Sasl\Options;

use Closure;

/**
* Options for the EXTERNAL challenge.
*
* - Client mode: optionally set the authzId.
* - Server mode: set the validate closure.
*
* @author Chad Sikorra <Chad.Sikorra@gmail.com>
*/
final class ExternalOptions implements ChallengeOptionsInterface
{
private ?string $authzId = null;

/**
* @var (Closure(?string $authzId): bool)|null
*/
private ?Closure $validate = null;

public function getAuthzId(): ?string
{
return $this->authzId;
}

public function setAuthzId(?string $authzId): self
{
$this->authzId = $authzId;

return $this;
}

/**
* @return (Closure(?string $authzId): bool)|null
*/
public function getValidate(): ?Closure
{
return $this->validate;
}

/**
* @param Closure(?string $authzId): bool $validate
*/
public function setValidate(Closure $validate): self
{
$this->validate = $validate;

return $this;
}
}
2 changes: 2 additions & 0 deletions src/FreeDSx/Sasl/Sasl.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use FreeDSx\Sasl\Mechanism\AnonymousMechanism;
use FreeDSx\Sasl\Mechanism\CramMD5Mechanism;
use FreeDSx\Sasl\Mechanism\DigestMD5Mechanism;
use FreeDSx\Sasl\Mechanism\ExternalMechanism;
use FreeDSx\Sasl\Mechanism\MechanismInterface;
use FreeDSx\Sasl\Mechanism\MechanismName;
use FreeDSx\Sasl\Mechanism\PlainMechanism;
Expand Down Expand Up @@ -93,6 +94,7 @@ private function initMechs(): void
MechanismName::CRAM_MD5->value => new CramMD5Mechanism(),
MechanismName::PLAIN->value => new PlainMechanism(),
MechanismName::ANONYMOUS->value => new AnonymousMechanism(),
MechanismName::EXTERNAL->value => new ExternalMechanism(),
];

foreach (MechanismName::cases() as $case) {
Expand Down
Loading
Loading