[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 declare(strict_types=1); 4 5 /* 6 * The MIT License (MIT) 7 * 8 * Copyright (c) 2014-2019 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 Webauthn\AttestationStatement; 15 16 use Assert\Assertion; 17 use Base64Url\Base64Url; 18 use CBOR\Decoder; 19 use CBOR\MapObject; 20 use CBOR\OtherObject\OtherObjectManager; 21 use CBOR\Tag\TagObjectManager; 22 use Cose\Algorithms; 23 use Cose\Key\Ec2Key; 24 use Cose\Key\Key; 25 use Cose\Key\OkpKey; 26 use Cose\Key\RsaKey; 27 use DateTimeImmutable; 28 use InvalidArgumentException; 29 use RuntimeException; 30 use Webauthn\AuthenticatorData; 31 use Webauthn\CertificateToolbox; 32 use Webauthn\MetadataService\MetadataStatementRepository; 33 use Webauthn\StringStream; 34 use Webauthn\TrustPath\CertificateTrustPath; 35 use Webauthn\TrustPath\EcdaaKeyIdTrustPath; 36 37 final class TPMAttestationStatementSupport implements AttestationStatementSupport 38 { 39 /** 40 * @var MetadataStatementRepository|null 41 */ 42 private $metadataStatementRepository; 43 44 public function name(): string 45 { 46 return 'tpm'; 47 } 48 49 public function __construct(?MetadataStatementRepository $metadataStatementRepository = null) 50 { 51 $this->metadataStatementRepository = $metadataStatementRepository; 52 } 53 54 public function load(array $attestation): AttestationStatement 55 { 56 Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); 57 Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported'); 58 foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) { 59 Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); 60 } 61 Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object'); 62 63 $certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']); 64 Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object'); 65 66 $pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']); 67 $pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit'); 68 $pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true); 69 $attestedName = $pubAreaAttestedNameAlg.$pubAreaHash; 70 Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name'); 71 72 $attestation['attStmt']['parsedCertInfo'] = $certInfo; 73 $attestation['attStmt']['parsedPubArea'] = $pubArea; 74 75 $certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']); 76 Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.'); 77 78 return AttestationStatement::createAttCA( 79 $this->name(), 80 $attestation['attStmt'], 81 new CertificateTrustPath($certificates) 82 ); 83 } 84 85 public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool 86 { 87 $attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash; 88 $attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true); 89 Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash'); 90 $this->checkUniquePublicKey( 91 $attestationStatement->get('parsedPubArea')['unique'], 92 $authenticatorData->getAttestedCredentialData()->getCredentialPublicKey() 93 ); 94 95 switch (true) { 96 case $attestationStatement->getTrustPath() instanceof CertificateTrustPath: 97 return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData); 98 case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath: 99 return $this->processWithECDAA(); 100 default: 101 throw new InvalidArgumentException('Unsupported attestation statement'); 102 } 103 } 104 105 private function checkUniquePublicKey(string $unique, string $cborPublicKey): void 106 { 107 $cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager()); 108 $publicKey = $cborDecoder->decode(new StringStream($cborPublicKey)); 109 Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key'); 110 $key = new Key($publicKey->getNormalizedData(false)); 111 112 switch ($key->type()) { 113 case Key::TYPE_OKP: 114 $uniqueFromKey = (new OkpKey($key->getData()))->x(); 115 break; 116 case Key::TYPE_EC2: 117 $ec2Key = new Ec2Key($key->getData()); 118 $uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y(); 119 break; 120 case Key::TYPE_RSA: 121 $uniqueFromKey = (new RsaKey($key->getData()))->n(); 122 break; 123 default: 124 throw new InvalidArgumentException('Invalid or unsupported key type.'); 125 } 126 127 Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value'); 128 } 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 private function checkPubArea(string $data): array 170 { 171 $pubArea = new StringStream($data); 172 173 $type = $pubArea->read(2); 174 175 $nameAlg = $pubArea->read(2); 176 177 $objectAttributes = $pubArea->read(4); 178 179 $authPolicyLength = unpack('n', $pubArea->read(2))[1]; 180 $authPolicy = $pubArea->read($authPolicyLength); 181 182 $parameters = $this->getParameters($type, $pubArea); 183 184 $uniqueLength = unpack('n', $pubArea->read(2))[1]; 185 $unique = $pubArea->read($uniqueLength); 186 Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.'); 187 $pubArea->close(); 188 189 return [ 190 'type' => $type, 191 'nameAlg' => $nameAlg, 192 'objectAttributes' => $objectAttributes, 193 'authPolicy' => $authPolicy, 194 'parameters' => $parameters, 195 'unique' => $unique, 196 ]; 197 } 198 199 private function getParameters(string $type, StringStream $stream): array 200 { 201 switch (bin2hex($type)) { 202 case '0001': 203 case '0014': 204 case '0016': 205 return [ 206 'symmetric' => $stream->read(2), 207 'scheme' => $stream->read(2), 208 'keyBits' => unpack('n', $stream->read(2))[1], 209 'exponent' => $this->getExponent($stream->read(4)), 210 ]; 211 case '0018': 212 return [ 213 'symmetric' => $stream->read(2), 214 'scheme' => $stream->read(2), 215 'curveId' => $stream->read(2), 216 'kdf' => $stream->read(2), 217 ]; 218 default: 219 throw new InvalidArgumentException('Unsupported type'); 220 } 221 } 222 223 private function getExponent(string $exponent): string 224 { 225 return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent; 226 } 227 228 private function convertCertificatesToPem(array $certificates): array 229 { 230 foreach ($certificates as $k => $v) { 231 $tmp = '-----BEGIN CERTIFICATE-----'.PHP_EOL; 232 $tmp .= chunk_split(base64_encode($v), 64, PHP_EOL); 233 $tmp .= '-----END CERTIFICATE-----'.PHP_EOL; 234 $certificates[$k] = $tmp; 235 } 236 237 return $certificates; 238 } 239 240 private function getTPMHash(string $nameAlg): string 241 { 242 switch (bin2hex($nameAlg)) { 243 case '0004': 244 return 'sha1'; //: "TPM_ALG_SHA1", 245 case '000b': 246 return 'sha256'; //: "TPM_ALG_SHA256", 247 case '000c': 248 return 'sha384'; //: "TPM_ALG_SHA384", 249 case '000d': 250 return 'sha512'; //: "TPM_ALG_SHA512", 251 default: 252 throw new InvalidArgumentException('Unsupported hash algorithm'); 253 } 254 } 255 256 private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool 257 { 258 $trustPath = $attestationStatement->getTrustPath(); 259 Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); 260 261 $certificates = $trustPath->getCertificates(); 262 if (null !== $this->metadataStatementRepository) { 263 $certificates = CertificateToolbox::checkAttestationMedata( 264 $attestationStatement, 265 $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), 266 $certificates, 267 $this->metadataStatementRepository 268 ); 269 } 270 271 // Check certificate CA chain and returns the Attestation Certificate 272 $this->checkCertificate($certificates[0], $authenticatorData); 273 274 // Get the COSE algorithm identifier and the corresponding OpenSSL one 275 $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg'); 276 $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier); 277 278 $result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier); 279 280 return 1 === $result; 281 } 282 283 private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void 284 { 285 $parsed = openssl_x509_parse($attestnCert); 286 Assertion::isArray($parsed, 'Invalid certificate'); 287 288 //Check version 289 Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version'); 290 291 //Check subject field is empty 292 Assertion::false(!isset($parsed['subject']) || !\is_array($parsed['subject']) || 0 !== \count($parsed['subject']), 'Invalid certificate name. The Subject should be empty'); 293 294 // Check period of validity 295 Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.'); 296 Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.'); 297 $startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']); 298 Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.'); 299 300 Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.'); 301 Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.'); 302 $endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']); 303 Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.'); 304 305 //Check extensions 306 Assertion::false(!isset($parsed['extensions']) || !\is_array($parsed['extensions']), 'Certificate extensions are missing'); 307 308 //Check subjectAltName 309 Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing'); 310 311 //Check extendedKeyUsage 312 Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing'); 313 Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid'); 314 315 // id-fido-gen-ce-aaguid OID check 316 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'); 317 318 // TODO: For attestationRoot in metadata.attestationRootCertificates, generate verification chain verifX5C by appending attestationRoot to the x5c. Try verifying verifX5C. If successful go to next step. If fail try next attestationRoot. If no attestationRoots left to try, fail. 319 } 320 321 private function processWithECDAA(): bool 322 { 323 throw new RuntimeException('ECDAA not supported'); 324 } 325 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |