Skip to content

Commit 3db3259

Browse files
authored
Fix wrong EdDSA key encoding (#437)
1 parent c53f585 commit 3db3259

7 files changed

+203
-173
lines changed

phpstan-baseline.neon

+25-10
Original file line numberDiff line numberDiff line change
@@ -1501,11 +1501,6 @@ parameters:
15011501
count: 1
15021502
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php
15031503

1504-
-
1505-
message: "#^Cannot access offset 1 on array\\|false\\.$#"
1506-
count: 3
1507-
path: src/webauthn/src/AttestationStatement/AttestationObjectLoader.php
1508-
15091504
-
15101505
message: "#^Parameter \\#1 \\$data of static method Webauthn\\\\TrustPath\\\\TrustPathLoader\\:\\:loadTrustPath\\(\\) expects array, mixed given\\.$#"
15111506
count: 1
@@ -1836,6 +1831,31 @@ parameters:
18361831
count: 1
18371832
path: src/webauthn/src/AuthenticatorAttestationResponseValidator.php
18381833

1834+
-
1835+
message: "#^Cannot access offset 1 on array\\|false\\.$#"
1836+
count: 2
1837+
path: src/webauthn/src/AuthenticatorDataLoader.php
1838+
1839+
-
1840+
message: "#^Parameter \\#1 \\$search of function str_replace expects array\\|string, string\\|false given\\.$#"
1841+
count: 1
1842+
path: src/webauthn/src/AuthenticatorDataLoader.php
1843+
1844+
-
1845+
message: "#^Parameter \\#2 \\$callback of function array_reduce expects callable\\(string, mixed\\)\\: string, Closure\\(string, string\\)\\: non\\-empty\\-string given\\.$#"
1846+
count: 1
1847+
path: src/webauthn/src/AuthenticatorDataLoader.php
1848+
1849+
-
1850+
message: "#^Parameter \\#2 \\$needle of function mb_strpos expects string, string\\|false given\\.$#"
1851+
count: 1
1852+
path: src/webauthn/src/AuthenticatorDataLoader.php
1853+
1854+
-
1855+
message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|false given\\.$#"
1856+
count: 1
1857+
path: src/webauthn/src/AuthenticatorDataLoader.php
1858+
18391859
-
18401860
message: """
18411861
#^Access to deprecated property \\$requireResidentKey of class Webauthn\\\\AuthenticatorSelectionCriteria\\:
@@ -1890,11 +1910,6 @@ parameters:
18901910
count: 1
18911911
path: src/webauthn/src/PublicKeyCredentialDescriptorCollection.php
18921912

1893-
-
1894-
message: "#^Cannot access offset 1 on array\\|false\\.$#"
1895-
count: 2
1896-
path: src/webauthn/src/PublicKeyCredentialLoader.php
1897-
18981913
-
18991914
message: "#^Parameter \\#1 \\$json of method Webauthn\\\\PublicKeyCredentialLoader\\:\\:loadArray\\(\\) expects array, mixed given\\.$#"
19001915
count: 1

src/webauthn/src/AttestationStatement/AttestationObjectLoader.php

+5-70
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,13 @@
66

77
use function array_key_exists;
88
use CBOR\Decoder;
9-
use CBOR\MapObject;
109
use CBOR\Normalizable;
1110
use function is_array;
12-
use function ord;
1311
use Psr\EventDispatcher\EventDispatcherInterface;
1412
use Psr\Log\LoggerInterface;
1513
use Psr\Log\NullLogger;
16-
use Symfony\Component\Uid\Uuid;
1714
use Throwable;
18-
use function unpack;
19-
use Webauthn\AttestedCredentialData;
20-
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
21-
use Webauthn\AuthenticatorData;
15+
use Webauthn\AuthenticatorDataLoader;
2216
use Webauthn\Event\AttestationObjectLoaded;
2317
use Webauthn\Exception\InvalidDataException;
2418
use Webauthn\MetadataService\CanLogData;
@@ -29,20 +23,13 @@
2923

3024
class AttestationObjectLoader implements CanDispatchEvents, CanLogData
3125
{
32-
private const FLAG_AT = 0b01000000;
33-
34-
private const FLAG_ED = 0b10000000;
35-
36-
private readonly Decoder $decoder;
37-
3826
private LoggerInterface $logger;
3927

4028
private EventDispatcherInterface $dispatcher;
4129

4230
public function __construct(
4331
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
4432
) {
45-
$this->decoder = Decoder::create();
4633
$this->logger = new NullLogger();
4734
$this->dispatcher = new NullEventDispatcher();
4835
}
@@ -65,7 +52,7 @@ public function load(string $data): AttestationObject
6552
]);
6653
$decodedData = Base64::decode($data);
6754
$stream = new StringStream($decodedData);
68-
$parsed = $this->decoder->decode($stream);
55+
$parsed = Decoder::create()->decode($stream);
6956

7057
$this->logger->info('Loading the Attestation Statement');
7158
$parsed instanceof Normalizable || throw InvalidDataException::create(
@@ -94,69 +81,17 @@ public function load(string $data): AttestationObject
9481
$attestationObject,
9582
'Invalid attestation object'
9683
);
97-
$authData = $attestationObject['authData'];
9884

9985
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
10086
$attestationStatement = $attestationStatementSupport->load($attestationObject);
10187
$this->logger->info('Attestation Statement loaded');
10288
$this->logger->debug('Attestation Statement loaded', [
10389
'attestationStatement' => $attestationStatement,
10490
]);
91+
$authData = $attestationObject['authData'];
92+
$authDataLoader = AuthenticatorDataLoader::create();
93+
$authenticatorData = $authDataLoader->load($authData);
10594

106-
$authDataStream = new StringStream($authData);
107-
$rp_id_hash = $authDataStream->read(32);
108-
$flags = $authDataStream->read(1);
109-
$signCount = $authDataStream->read(4);
110-
$signCount = unpack('N', $signCount);
111-
$this->logger->debug(sprintf('Signature counter: %d', $signCount[1]));
112-
113-
$attestedCredentialData = null;
114-
if (0 !== (ord($flags) & self::FLAG_AT)) {
115-
$this->logger->info('Attested Credential Data is present');
116-
$aaguid = Uuid::fromBinary($authDataStream->read(16));
117-
$credentialLength = $authDataStream->read(2);
118-
$credentialLength = unpack('n', $credentialLength);
119-
$credentialId = $authDataStream->read($credentialLength[1]);
120-
$credentialPublicKey = $this->decoder->decode($authDataStream);
121-
$credentialPublicKey instanceof MapObject || throw InvalidDataException::create(
122-
$credentialPublicKey,
123-
'The data does not contain a valid credential public key.'
124-
);
125-
$attestedCredentialData = new AttestedCredentialData(
126-
$aaguid,
127-
$credentialId,
128-
(string) $credentialPublicKey
129-
);
130-
$this->logger->info('Attested Credential Data loaded');
131-
$this->logger->debug('Attested Credential Data loaded', [
132-
'at' => $attestedCredentialData,
133-
]);
134-
}
135-
136-
$extension = null;
137-
if (0 !== (ord($flags) & self::FLAG_ED)) {
138-
$this->logger->info('Extension Data loaded');
139-
$extension = $this->decoder->decode($authDataStream);
140-
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
141-
$this->logger->info('Extension Data loaded');
142-
$this->logger->debug('Extension Data loaded', [
143-
'ed' => $extension,
144-
]);
145-
}
146-
$authDataStream->isEOF() || throw InvalidDataException::create(
147-
null,
148-
'Invalid authentication data. Presence of extra bytes.'
149-
);
150-
$authDataStream->close();
151-
152-
$authenticatorData = new AuthenticatorData(
153-
$authData,
154-
$rp_id_hash,
155-
$flags,
156-
$signCount[1],
157-
$attestedCredentialData,
158-
$extension
159-
);
16095
$attestationObject = new AttestationObject($data, $attestationStatement, $authenticatorData);
16196
$this->logger->info('Attestation Object loaded');
16297
$this->logger->debug('Attestation Object', [

src/webauthn/src/AuthenticatorData.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212
*/
1313
class AuthenticatorData
1414
{
15-
private const FLAG_UP = 0b00000001;
15+
final public const FLAG_UP = 0b00000001;
1616

17-
private const FLAG_RFU1 = 0b00000010;
17+
final public const FLAG_RFU1 = 0b00000010;
1818

19-
private const FLAG_UV = 0b00000100;
19+
final public const FLAG_UV = 0b00000100;
2020

21-
private const FLAG_RFU2 = 0b00111000;
21+
final public const FLAG_RFU2 = 0b00111000;
2222

23-
private const FLAG_AT = 0b01000000;
23+
final public const FLAG_AT = 0b01000000;
2424

25-
private const FLAG_ED = 0b10000000;
25+
final public const FLAG_ED = 0b10000000;
2626

2727
public function __construct(
2828
protected string $authData,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn;
6+
7+
use CBOR\ByteStringObject;
8+
use CBOR\Decoder;
9+
use CBOR\ListObject;
10+
use CBOR\MapObject;
11+
use CBOR\NegativeIntegerObject;
12+
use CBOR\TextStringObject;
13+
use CBOR\UnsignedIntegerObject;
14+
use function chr;
15+
use function ord;
16+
use Symfony\Component\Uid\Uuid;
17+
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
18+
use Webauthn\Exception\InvalidDataException;
19+
20+
final class AuthenticatorDataLoader
21+
{
22+
private readonly Decoder $decoder;
23+
24+
private function __construct()
25+
{
26+
$this->decoder = Decoder::create();
27+
}
28+
29+
public static function create(): self
30+
{
31+
return new self();
32+
}
33+
34+
public function load(string $authData): AuthenticatorData
35+
{
36+
$authData = $this->fixIncorrectEdDSAKey($authData);
37+
$authDataStream = new StringStream($authData);
38+
$rp_id_hash = $authDataStream->read(32);
39+
$flags = $authDataStream->read(1);
40+
$signCount = $authDataStream->read(4);
41+
$signCount = unpack('N', $signCount);
42+
43+
$attestedCredentialData = null;
44+
if (0 !== (ord($flags) & AuthenticatorData::FLAG_AT)) {
45+
$aaguid = Uuid::fromBinary($authDataStream->read(16));
46+
$credentialLength = $authDataStream->read(2);
47+
$credentialLength = unpack('n', $credentialLength);
48+
$credentialId = $authDataStream->read($credentialLength[1]);
49+
$credentialPublicKey = $this->decoder->decode($authDataStream);
50+
$credentialPublicKey instanceof MapObject || throw InvalidDataException::create(
51+
$authData,
52+
'The data does not contain a valid credential public key.'
53+
);
54+
$attestedCredentialData = new AttestedCredentialData(
55+
$aaguid,
56+
$credentialId,
57+
(string) $credentialPublicKey
58+
);
59+
}
60+
61+
$extension = null;
62+
if (0 !== (ord($flags) & AuthenticatorData::FLAG_ED)) {
63+
$extension = $this->decoder->decode($authDataStream);
64+
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
65+
}
66+
$authDataStream->isEOF() || throw InvalidDataException::create(
67+
$authData,
68+
'Invalid authentication data. Presence of extra bytes.'
69+
);
70+
$authDataStream->close();
71+
72+
return new AuthenticatorData(
73+
$authData,
74+
$rp_id_hash,
75+
$flags,
76+
$signCount[1],
77+
$attestedCredentialData,
78+
$extension
79+
);
80+
}
81+
82+
private function fixIncorrectEdDSAKey(string $data): string
83+
{
84+
$needle = hex2bin('a301634f4b500327206745643235353139');
85+
$correct = hex2bin('a401634f4b500327206745643235353139');
86+
$position = mb_strpos($data, $needle, 0, '8bit');
87+
if ($position === false) {
88+
return $data;
89+
}
90+
91+
$begin = mb_substr($data, 0, $position, '8bit');
92+
$end = mb_substr($data, $position, null, '8bit');
93+
$end = str_replace($needle, $correct, $end);
94+
$cbor = new StringStream($end);
95+
$badKey = $this->decoder->decode($cbor);
96+
97+
($badKey instanceof MapObject && $cbor->isEOF()) || throw InvalidDataException::create(
98+
$end,
99+
'Invalid authentication data. Presence of extra bytes.'
100+
);
101+
$badX = $badKey->get(-2);
102+
$badX instanceof ListObject || throw InvalidDataException::create($end, 'Invalid authentication data.');
103+
$keyBytes = array_reduce(
104+
$badX->normalize(),
105+
static fn (string $carry, string $item): string => $carry . chr((int) $item),
106+
''
107+
);
108+
$correctX = ByteStringObject::create($keyBytes);
109+
$correctKey = MapObject::create()
110+
->add(UnsignedIntegerObject::create(1), ByteStringObject::create('OKP'))
111+
->add(UnsignedIntegerObject::create(3), NegativeIntegerObject::create(-8))
112+
->add(NegativeIntegerObject::create(-1), TextStringObject::create('Ed25519'))
113+
->add(NegativeIntegerObject::create(-2), $correctX);
114+
115+
return $begin . $correctKey;
116+
}
117+
}

0 commit comments

Comments
 (0)