Skip to content

Commit

Permalink
Implement update to downgrade proection (XEP-0474)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabiang committed Feb 14, 2025
1 parent d410630 commit 246b7b1
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 75 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ jobs:
delay: 60000
log: true

# - name: Wait for second XMPP to become available
# uses: iFaxity/wait-on-action@v1
# with:
# resource: tcp:localhost:25222
# timeout: 1800000
# interval: 10000
# delay: 60000
# log: true
- name: Wait for second XMPP to become available
uses: iFaxity/wait-on-action@v1
with:
resource: tcp:localhost:25222
timeout: 1800000
interval: 10000
delay: 60000
log: true

- name: Run test suite
run: ./vendor/bin/behat
Expand Down
11 changes: 2 additions & 9 deletions behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,13 @@ default:
- Fabiang\SASL\Behat\XMPPContext:
- localhost
- 15222
# Extra test server for https://dyn.eightysoft.de/xeps/xep-0474.html
- 25222
- localhost
- testuser
- testpass
- "%paths.base%/tests/log/features/"
- tlsv1.2
# Extra test server for https://dyn.eightysoft.de/xeps/xep-0474.html
# - Fabiang\SASL\Behat\XMPPContext:
# - localhost
# - 25222
# - localhost
# - testuser
# - testpass
# - "%paths.base%/tests/log/features/"
# - tlsv1.3
- Fabiang\SASL\Behat\POP3Context:
- localhost
- 1110
Expand Down
26 changes: 13 additions & 13 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ services:
- CTL_ON_CREATE=register testuser localhost testpass

# Extra test server for https://dyn.eightysoft.de/xeps/xep-0474.html
# xmpp_source:
# build:
# context: https://github.com/processone/docker-ejabberd.git#master:ecs
# args:
# - VERSION=133d52d04023d603283a7796c46bc40ffc7cd3c2
# volumes:
# - "./tests/config/ejabberd/ejabberd.yml:/home/ejabberd/conf/ejabberd.yml"
# - "./tests/config/ejabberd/ejabberd.db:/home/ejabberd/database/ejabberd.db"
# ports:
# - "25222:5222"
# - "25223:5223"
# environment:
# - CTL_ON_CREATE=register testuser localhost testpass
xmpp_source:
build:
context: https://github.com/processone/docker-ejabberd.git#master:ecs
args:
- VERSION=ceee3d3be1fc1053e61bbc81881bd40dbbbc1e89
volumes:
- "./tests/config/ejabberd/ejabberd.yml:/home/ejabberd/conf/ejabberd.yml"
- "./tests/config/ejabberd/ejabberd.db:/home/ejabberd/database/ejabberd.db"
ports:
- "25222:5222"
- "25223:5223"
environment:
- CTL_ON_CREATE=register testuser localhost testpass

mail:
image: dovecot/dovecot:${DOVECOT_VERSION:-2.3.18}
Expand Down
10 changes: 5 additions & 5 deletions src/Authentication/AbstractAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ protected function generateCnonce(): string
/**
* Generate downgrade protection string
*/
protected function generateDowngradeProtectionVerification(): string
protected function generateDowngradeProtectionVerification(string $groupDelimiter, string $delimiter): string
{
$downgradeProtectionOptions = $this->options->getDowngradeProtection();
if ($downgradeProtectionOptions === null) {
Expand All @@ -108,12 +108,12 @@ protected function generateDowngradeProtectionVerification(): string
return '';
}

usort($allowedMechanisms, [$this, 'sortOctetCollation']);
usort($allowedChannelBindings, [$this, 'sortOctetCollation']);
usort($allowedMechanisms, $this->sortOctetCollation(...));
usort($allowedChannelBindings, $this->sortOctetCollation(...));

$protect = implode(',', $allowedMechanisms);
$protect = implode($delimiter, $allowedMechanisms);
if (count($allowedChannelBindings) > 0) {
$protect .= '|' . implode(',', $allowedChannelBindings);
$protect .= $groupDelimiter . implode($delimiter, $allowedChannelBindings);
}
return $protect;
}
Expand Down
67 changes: 55 additions & 12 deletions src/Authentication/SCRAM.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
class SCRAM extends AbstractAuthentication implements ChallengeAuthenticationInterface, VerificationInterface
{
private string $hashAlgo;

private ?string $gs2Header = null;
private ?string $cnonce = null;
private ?string $firstMessageBare = null;
Expand All @@ -80,7 +79,9 @@ public function __construct(Options $options, string $hash)
// For instance "sha1" is accepted, while the registered hash name should be "SHA-1".
$replaced = preg_replace('#^sha-(\d+)#i', 'sha\1', $hash);
if ($replaced === null) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException("Could not normalize hash '$hash'");
// @codeCoverageIgnoreEnd
}
$normalizedHash = strtolower($replaced);

Expand Down Expand Up @@ -176,22 +177,32 @@ private function generateResponse(
/* @psalm-var array<int,string> $matches */
$matches = [];

$downGradeProtectionRegexp = '(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)';

$serverMessageRegexp = "#^r=(?<nonce>[\x21-\x2B\x2D-\x7E/]+)"
. ",s=(?<salt>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?)"
. ",i=(?<iteration>[0-9]*)"
. "(?:,d=(?<downgradeProtection>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)))?"
. "(,[A-Za-z]=[^,])*$#";
. "(?:,d=(?<downgradeProtection>$downGradeProtectionRegexp))?"
. "(?:,h=(?<downgradeProtectionSecure>$downGradeProtectionRegexp))?"
. "(?<additionalAttr>(?:,[A-Za-z]=[^,]+)*)$#";

if ($this->cnonce === null ||
$this->gs2Header === null ||
$this->firstMessageBare === null ||
!preg_match($serverMessageRegexp, $challenge, $matches)) {
! preg_match($serverMessageRegexp, $challenge, $matches)) {
return false;
}

$additionalAttribute = $this->parseAdditionalAttributes($matches['additionalAttr']);

if (isset($additionalAttribute['m'])) {
return false;
}

$nonce = $matches['nonce'];
$salt = base64_decode($matches['salt']);
if (!$salt) {

if (! $salt) {
// Invalid Base64.
return false;
}
Expand All @@ -203,8 +214,14 @@ private function generateResponse(
return false;
}

if ($matches['downgradeProtection'] !== '') {
if (!$this->downgradeProtection($matches['downgradeProtection'])) {
if (! empty($matches['downgradeProtectionSecure'])) {
if (! $this->downgradeProtection($matches['downgradeProtectionSecure'], "\x1f", "\x1e")) {
return false;
}
}

if (! empty($matches['downgradeProtection'])) {
if (! $this->downgradeProtection($matches['downgradeProtection'], '|', ',')) {
return false;
}
}
Expand All @@ -224,13 +241,20 @@ private function generateResponse(
return $finalMessage . $proof;
}

private function downgradeProtection(string $expectedDowngradeProtectionHash): bool
{
private function downgradeProtection(
string $expectedDowngradeProtectionHash,
string $groupDelimiter,
string $delimiter
): bool {
if ($this->options->getDowngradeProtection() === null) {
return true;
}

$actualDgPHash = base64_encode($this->hash($this->generateDowngradeProtectionVerification()));
$actualDgPHash = base64_encode(
$this->hash(
$this->generateDowngradeProtectionVerification($groupDelimiter, $delimiter)
)
);
return $expectedDowngradeProtectionHash === $actualDgPHash;
}

Expand Down Expand Up @@ -268,16 +292,23 @@ private function hi(
*/
public function verify(string $data): bool
{
$verifierRegexp = '#^v=(?<verifier>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?)$#';
$verifierRegexp = '#^v=(?<verifier>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?)'
. '(?<additionalAttr>(?:,[A-Za-z]=[^,]+)*)$#';

$matches = [];
if (empty($this->saltedSecret) ||
$this->authMessage === null ||
!preg_match($verifierRegexp, $data, $matches)) {
! preg_match($verifierRegexp, $data, $matches)) {
// This cannot be an outcome, you never sent the challenge's response.
return false;
}

$additionalAttribute = $this->parseAdditionalAttributes($matches['additionalAttr']);

if (isset($additionalAttribute['m'])) {
return false;
}

$saltedSecret = $this->saltedSecret->getValue();

$verifier = $matches['verifier'];
Expand All @@ -288,6 +319,18 @@ public function verify(string $data): bool
return $proposedServerSignature === $serverSignature;
}

private function parseAdditionalAttributes(string $addAttr): array
{
return array_column(
array_map(
fn($v) => explode('=', trim($v), 2),
array_filter(explode(',', $addAttr))
),
1,
0
);
}

private function hash(string $data): string
{
return hash($this->hashAlgo, $data, true);
Expand Down
11 changes: 0 additions & 11 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,4 @@ public function getDowngradeProtection(): ?DowngradeProtectionOptions
{
return $this->downgradeProtection;
}

public function toArray(): array
{
return [
'authcid' => $this->getAuthcid(),
'secret' => $this->getSecret(),
'authzid' => $this->getAuthzid(),
'service' => $this->getService(),
'hostname' => $this->getHostname(),
];
}
}
7 changes: 4 additions & 3 deletions tests/features/bootstrap/AbstractContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,21 @@
abstract class AbstractContext
{
protected string $hostname;
protected int $port;
protected int $port1;
protected int $port2;
protected string $username;
protected string $password;

protected string $logdir;
protected $stream;
protected $logfile;

protected function connect(): void
protected function connect(int $port): void
{
$errno = null;
$errstr = null;

$connectionString = "tcp://{$this->hostname}:{$this->port}";
$connectionString = "tcp://{$this->hostname}:{$port}";

$context = stream_context_create([
'ssl' => [
Expand Down
4 changes: 2 additions & 2 deletions tests/features/bootstrap/POP3Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class POP3Context extends AbstractContext implements Context, SnippetAcceptingCo
public function __construct(string $hostname, string $port, string $username, string $password, string $logdir)
{
$this->hostname = $hostname;
$this->port = (int) $port;
$this->port1 = (int) $port;
$this->username = $username;
$this->password = $password;

Expand All @@ -85,7 +85,7 @@ public function __construct(string $hostname, string $port, string $username, st
#[Given('Connection to pop3 server')]
public function connectionToPopServer(): void
{
$this->connect();
$this->connect($this->port1);
Assert::assertSame("+OK Dovecot ready.\r\n", $this->read());
}

Expand Down
22 changes: 16 additions & 6 deletions tests/features/bootstrap/XMPPContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class XMPPContext extends AbstractContext implements Context, SnippetAcceptingCo
* context constructor through behat.yml.
*
* @param string $hostname Hostname for connection
* @param integer $port
* @param integer $port1
* @param integer $port2
* @param string $domain
* @param string $username Domain name of server (important for connecting)
* @param string $password
Expand All @@ -81,15 +82,17 @@ class XMPPContext extends AbstractContext implements Context, SnippetAcceptingCo
*/
public function __construct(
string $hostname,
string $port,
string $port1,
string $port2,
string $domain,
string $username,
string $password,
string $logdir,
string $tlsversion = 'tlsv1.2'
) {
$this->hostname = $hostname;
$this->port = (int) $port;
$this->port1 = (int) $port1;
$this->port2 = (int) $port2;
$this->domain = $domain;
$this->username = $username;
$this->password = $password;
Expand All @@ -115,10 +118,17 @@ private function getOptions(): Options
);
}

#[Given('Connection to xmpp server')]
public function connectionToXmppServer(): void
#[Given('Connection to XMPP server')]
public function connectionToXMPPServer(): void
{
$this->connect();
$this->connect($this->port1);
$this->sendStreamStart();
}

#[Given('Connection to second XMPP server')]
public function connectionToSecondXMPPServer(): void
{
$this->connect($this->port2);
$this->sendStreamStart();
}

Expand Down
4 changes: 2 additions & 2 deletions tests/features/downgrade_protection.feature
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@xmpp @downgradeProtection
Feature: Authentication with a xmpp server
Feature: Authentication with a XMPP server

Background:
Given Connection to xmpp server
Given Connection to XMPP server
And Connection is encrypted by STARTTLS

@scramsha1
Expand Down
Loading

0 comments on commit 246b7b1

Please sign in to comment.