PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / lib / packages / Webauthn / AttestationStatement / TPMAttestationStatementSupport.php

TPMAttestationStatementSupport.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at lib/packages/Webauthn/AttestationStatement/TPMAttestationStatementSupport.php

310 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 /*
6 * The MIT License (MIT)
7 *
8 * Copyright (c) 2014-2021 Spomky-Labs
9 *
10 * This software may be modified and distributed under the terms
11 * of the MIT license. See the LICENSE file for details.
12 */
13
14 namespace WP_DEFENDER_VENDOR\Webauthn\AttestationStatement;
15
16 use WP_DEFENDER_VENDOR\Assert\Assertion;
17 use WP_DEFENDER_VENDOR\Base64Url\Base64Url;
18 use WP_DEFENDER_VENDOR\CBOR\Decoder;
19 use WP_DEFENDER_VENDOR\CBOR\MapObject;
20 use WP_DEFENDER_VENDOR\CBOR\OtherObject\OtherObjectManager;
21 use WP_DEFENDER_VENDOR\CBOR\Tag\TagObjectManager;
22 use WP_DEFENDER_VENDOR\Cose\Algorithms;
23 use WP_DEFENDER_VENDOR\Cose\Key\Ec2Key;
24 use WP_DEFENDER_VENDOR\Cose\Key\Key;
25 use WP_DEFENDER_VENDOR\Cose\Key\OkpKey;
26 use WP_DEFENDER_VENDOR\Cose\Key\RsaKey;
27 use function count;
28 use function in_array;
29 use InvalidArgumentException;
30 use function is_array;
31 use RuntimeException;
32 use WP_DEFENDER_VENDOR\Safe\DateTimeImmutable;
33 use function WP_DEFENDER_VENDOR\Safe\sprintf;
34 use function WP_DEFENDER_VENDOR\Safe\unpack;
35 use WP_DEFENDER_VENDOR\Webauthn\AuthenticatorData;
36 use WP_DEFENDER_VENDOR\Webauthn\CertificateToolbox;
37 use WP_DEFENDER_VENDOR\Webauthn\StringStream;
38 use WP_DEFENDER_VENDOR\Webauthn\TrustPath\CertificateTrustPath;
39 use WP_DEFENDER_VENDOR\Webauthn\TrustPath\EcdaaKeyIdTrustPath;
40
41 final class TPMAttestationStatementSupport implements AttestationStatementSupport
42 {
43 public function name(): string
44 {
45 return 'tpm';
46 }
47
48 /**
49 * @param mixed[] $attestation
50 */
51 public function load(array $attestation): AttestationStatement
52 {
53 Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
54 Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported');
55 foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
56 Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
57 }
58 Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object');
59
60 $certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
61 Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object');
62
63 $pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
64 $pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit');
65 $pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true);
66 $attestedName = $pubAreaAttestedNameAlg.$pubAreaHash;
67 Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name');
68
69 $attestation['attStmt']['parsedCertInfo'] = $certInfo;
70 $attestation['attStmt']['parsedPubArea'] = $pubArea;
71
72 $certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
73 Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
74
75 return AttestationStatement::createAttCA(
76 $this->name(),
77 $attestation['attStmt'],
78 new CertificateTrustPath($certificates)
79 );
80 }
81
82 public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
83 {
84 $attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash;
85 $attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true);
86 Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash');
87 $this->checkUniquePublicKey(
88 $attestationStatement->get('parsedPubArea')['unique'],
89 $authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()
90 );
91
92 switch (true) {
93 case $attestationStatement->getTrustPath() instanceof CertificateTrustPath:
94 return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
95 case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath:
96 return $this->processWithECDAA();
97 default:
98 throw new InvalidArgumentException('Unsupported attestation statement');
99 }
100 }
101
102 private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
103 {
104 $cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
105 $publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
106 Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key');
107 $key = new Key($publicKey->getNormalizedData(false));
108
109 switch ($key->type()) {
110 case Key::TYPE_OKP:
111 $uniqueFromKey = (new OkpKey($key->getData()))->x();
112 break;
113 case Key::TYPE_EC2:
114 $ec2Key = new Ec2Key($key->getData());
115 $uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y();
116 break;
117 case Key::TYPE_RSA:
118 $uniqueFromKey = (new RsaKey($key->getData()))->n();
119 break;
120 default:
121 throw new InvalidArgumentException('Invalid or unsupported key type.');
122 }
123
124 Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value');
125 }
126
127 /**
128 * @return mixed[]
129 */
130 private function checkCertInfo(string $data): array
131 {
132 $certInfo = new StringStream($data);
133
134 $magic = $certInfo->read(4);
135 Assertion::eq('ff544347', bin2hex($magic), 'Invalid attestation object');
136
137 $type = $certInfo->read(2);
138
139 $qualifiedSignerLength = unpack('n', $certInfo->read(2))[1];
140 $qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored
141
142 $extraDataLength = unpack('n', $certInfo->read(2))[1];
143 $extraData = $certInfo->read($extraDataLength);
144
145 $clockInfo = $certInfo->read(17); //Ignore
146
147 $firmwareVersion = $certInfo->read(8);
148
149 $attestedNameLength = unpack('n', $certInfo->read(2))[1];
150 $attestedName = $certInfo->read($attestedNameLength);
151
152 $attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1];
153 $attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore
154 Assertion::true($certInfo->isEOF(), 'Invalid certificate information. Presence of extra bytes.');
155 $certInfo->close();
156
157 return [
158 'magic' => $magic,
159 'type' => $type,
160 'qualifiedSigner' => $qualifiedSigner,
161 'extraData' => $extraData,
162 'clockInfo' => $clockInfo,
163 'firmwareVersion' => $firmwareVersion,
164 'attestedName' => $attestedName,
165 'attestedQualifiedName' => $attestedQualifiedName,
166 ];
167 }
168
169 /**
170 * @return mixed[]
171 */
172 private function checkPubArea(string $data): array
173 {
174 $pubArea = new StringStream($data);
175
176 $type = $pubArea->read(2);
177
178 $nameAlg = $pubArea->read(2);
179
180 $objectAttributes = $pubArea->read(4);
181
182 $authPolicyLength = unpack('n', $pubArea->read(2))[1];
183 $authPolicy = $pubArea->read($authPolicyLength);
184
185 $parameters = $this->getParameters($type, $pubArea);
186
187 $uniqueLength = unpack('n', $pubArea->read(2))[1];
188 $unique = $pubArea->read($uniqueLength);
189 Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.');
190 $pubArea->close();
191
192 return [
193 'type' => $type,
194 'nameAlg' => $nameAlg,
195 'objectAttributes' => $objectAttributes,
196 'authPolicy' => $authPolicy,
197 'parameters' => $parameters,
198 'unique' => $unique,
199 ];
200 }
201
202 /**
203 * @return mixed[]
204 */
205 private function getParameters(string $type, StringStream $stream): array
206 {
207 switch (bin2hex($type)) {
208 case '0001':
209 case '0014':
210 case '0016':
211 return [
212 'symmetric' => $stream->read(2),
213 'scheme' => $stream->read(2),
214 'keyBits' => unpack('n', $stream->read(2))[1],
215 'exponent' => $this->getExponent($stream->read(4)),
216 ];
217 case '0018':
218 return [
219 'symmetric' => $stream->read(2),
220 'scheme' => $stream->read(2),
221 'curveId' => $stream->read(2),
222 'kdf' => $stream->read(2),
223 ];
224 default:
225 throw new InvalidArgumentException('Unsupported type');
226 }
227 }
228
229 private function getExponent(string $exponent): string
230 {
231 return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent;
232 }
233
234 private function getTPMHash(string $nameAlg): string
235 {
236 switch (bin2hex($nameAlg)) {
237 case '0004':
238 return 'sha1'; //: "TPM_ALG_SHA1",
239 case '000b':
240 return 'sha256'; //: "TPM_ALG_SHA256",
241 case '000c':
242 return 'sha384'; //: "TPM_ALG_SHA384",
243 case '000d':
244 return 'sha512'; //: "TPM_ALG_SHA512",
245 default:
246 throw new InvalidArgumentException('Unsupported hash algorithm');
247 }
248 }
249
250 private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
251 {
252 $trustPath = $attestationStatement->getTrustPath();
253 Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
254
255 $certificates = $trustPath->getCertificates();
256
257 // Check certificate CA chain and returns the Attestation Certificate
258 $this->checkCertificate($certificates[0], $authenticatorData);
259
260 // Get the COSE algorithm identifier and the corresponding OpenSSL one
261 $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
262 $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
263
264 $result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
265
266 return 1 === $result;
267 }
268
269 private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
270 {
271 $parsed = openssl_x509_parse($attestnCert);
272 Assertion::isArray($parsed, 'Invalid certificate');
273
274 //Check version
275 Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
276
277 //Check subject field is empty
278 Assertion::false(!isset($parsed['subject']) || !is_array($parsed['subject']) || 0 !== count($parsed['subject']), 'Invalid certificate name. The Subject should be empty');
279
280 // Check period of validity
281 Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.');
282 Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.');
283 $startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
284 Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.');
285
286 Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.');
287 Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.');
288 $endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
289 Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.');
290
291 //Check extensions
292 Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
293
294 //Check subjectAltName
295 Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing');
296
297 //Check extendedKeyUsage
298 Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing');
299 Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid');
300
301 // id-fido-gen-ce-aaguid OID check
302 Assertion::false(in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
303 }
304
305 private function processWithECDAA(): bool
306 {
307 throw new RuntimeException('ECDAA not supported');
308 }
309 }
310