[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package Joomla.Plugin 5 * @subpackage Multifactorauth.webauthn 6 * 7 * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 * @copyright (C) 2014-2019 Spomky-Labs 10 * @license This software may be modified and distributed under the terms 11 * of the MIT license. 12 * See libraries/vendor/web-auth/webauthn-lib/LICENSE 13 */ 14 15 namespace Joomla\Plugin\Multifactorauth\Webauthn\Hotfix; 16 17 use Assert\Assertion; 18 use CBOR\Decoder; 19 use CBOR\OtherObject\OtherObjectManager; 20 use CBOR\Tag\TagObjectManager; 21 use Cose\Algorithms; 22 use Cose\Key\Ec2Key; 23 use Cose\Key\Key; 24 use Cose\Key\RsaKey; 25 use FG\ASN1\ASNObject; 26 use FG\ASN1\ExplicitlyTaggedObject; 27 use FG\ASN1\Universal\OctetString; 28 use FG\ASN1\Universal\Sequence; 29 use Webauthn\AttestationStatement\AttestationStatement; 30 use Webauthn\AttestationStatement\AttestationStatementSupport; 31 use Webauthn\AuthenticatorData; 32 use Webauthn\CertificateToolbox; 33 use Webauthn\MetadataService\MetadataStatementRepository; 34 use Webauthn\StringStream; 35 use Webauthn\TrustPath\CertificateTrustPath; 36 37 // phpcs:disable PSR1.Files.SideEffects 38 \defined('_JEXEC') or die; 39 // phpcs:enable PSR1.Files.SideEffects 40 41 /** 42 * We had to fork the key attestation support object from the WebAuthn server package to address an 43 * issue with PHP 8. 44 * 45 * We are currently using an older version of the WebAuthn library (2.x) which was written before 46 * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of 47 * Joomla's Semantic Versioning promise. 48 * 49 * The AndroidKeyAttestationStatementSupport class forces an assertion on the result of the 50 * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with 51 * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a 52 * result, you cannot use Android or FIDO U2F keys with WebAuthn. 53 * 54 * The assertion check is in a private method, therefore we have to fork both attestation support 55 * class to change the assertion. The assertion takes place through a third party library we cannot 56 * (and should not!) modify. 57 * 58 * @since 4.2.0 59 * 60 * @deprecated 5.0 We will upgrade the WebAuthn library to version 3 or later and this will go away. 61 */ 62 final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport 63 { 64 /** 65 * @var Decoder 66 * @since 4.2.0 67 */ 68 private $decoder; 69 70 /** 71 * @var MetadataStatementRepository|null 72 * @since 4.2.0 73 */ 74 private $metadataStatementRepository; 75 76 /** 77 * @param Decoder|null $decoder Obvious 78 * @param MetadataStatementRepository|null $metadataStatementRepository Obvious 79 * 80 * @since 4.2.0 81 */ 82 public function __construct( 83 ?Decoder $decoder = null, 84 ?MetadataStatementRepository $metadataStatementRepository = null 85 ) { 86 if ($decoder !== null) { 87 @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); 88 } 89 90 if ($metadataStatementRepository === null) { 91 @trigger_error( 92 'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', 93 E_USER_DEPRECATED 94 ); 95 } 96 97 $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); 98 $this->metadataStatementRepository = $metadataStatementRepository; 99 } 100 101 /** 102 * @return string 103 * @since 4.2.0 104 */ 105 public function name(): string 106 { 107 return 'android-key'; 108 } 109 110 /** 111 * @param array $attestation Obvious 112 * 113 * @return AttestationStatement 114 * @throws \Assert\AssertionFailedException 115 * @since 4.2.0 116 */ 117 public function load(array $attestation): AttestationStatement 118 { 119 Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); 120 121 foreach (['sig', 'x5c', 'alg'] as $key) { 122 Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); 123 } 124 125 $certificates = $attestation['attStmt']['x5c']; 126 127 Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); 128 Assertion::greaterThan(\count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.'); 129 Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); 130 131 $certificates = CertificateToolbox::convertAllDERToPEM($certificates); 132 133 return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); 134 } 135 136 /** 137 * @param string $clientDataJSONHash Obvious 138 * @param AttestationStatement $attestationStatement Obvious 139 * @param AuthenticatorData $authenticatorData Obvious 140 * 141 * @return boolean 142 * @throws \Assert\AssertionFailedException 143 * @since 4.2.0 144 */ 145 public function isValid( 146 string $clientDataJSONHash, 147 AttestationStatement $attestationStatement, 148 AuthenticatorData $authenticatorData 149 ): bool { 150 $trustPath = $attestationStatement->getTrustPath(); 151 Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); 152 153 $certificates = $trustPath->getCertificates(); 154 155 if ($this->metadataStatementRepository !== null) { 156 $certificates = CertificateToolbox::checkAttestationMedata( 157 $attestationStatement, 158 $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), 159 $certificates, 160 $this->metadataStatementRepository 161 ); 162 } 163 164 // Decode leaf attestation certificate 165 $leaf = $certificates[0]; 166 $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData); 167 168 $signedData = $authenticatorData->getAuthData() . $clientDataJSONHash; 169 $alg = $attestationStatement->get('alg'); 170 171 return openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg)) === 1; 172 } 173 174 /** 175 * @param string $certificate Obvious 176 * @param string $clientDataHash Obvious 177 * @param AuthenticatorData $authenticatorData Obvious 178 * 179 * @return void 180 * @throws \Assert\AssertionFailedException 181 * @throws \FG\ASN1\Exception\ParserException 182 * @since 4.2.0 183 */ 184 private function checkCertificateAndGetPublicKey( 185 string $certificate, 186 string $clientDataHash, 187 AuthenticatorData $authenticatorData 188 ): void { 189 $resource = openssl_pkey_get_public($certificate); 190 191 if (version_compare(PHP_VERSION, '8.0', 'lt')) { 192 Assertion::isResource($resource, 'Unable to read the certificate'); 193 } else { 194 /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ 195 Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate'); 196 } 197 198 $details = openssl_pkey_get_details($resource); 199 Assertion::isArray($details, 'Unable to read the certificate'); 200 201 // Check that authData publicKey matches the public key in the attestation certificate 202 $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); 203 Assertion::notNull($attestedCredentialData, 'No attested credential data found'); 204 $publicKeyData = $attestedCredentialData->getCredentialPublicKey(); 205 Assertion::notNull($publicKeyData, 'No attested public key found'); 206 $publicDataStream = new StringStream($publicKeyData); 207 $coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false); 208 Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.'); 209 $publicDataStream->close(); 210 $publicKey = Key::createFromData($coseKey); 211 212 Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type'); 213 Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key'); 214 215 $certDetails = openssl_x509_parse($certificate); 216 217 // Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions 218 Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension'); 219 Assertion::isArray($certDetails['extensions'], 'The certificate has no extension'); 220 Assertion::keyExists( 221 $certDetails['extensions'], 222 '1.3.6.1.4.1.11129.2.1.17', 223 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing' 224 ); 225 $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17']; 226 $extensionAsAsn1 = ASNObject::fromBinary($extension); 227 Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 228 $objects = $extensionAsAsn1->getChildren(); 229 230 // Check that attestationChallenge is set to the clientDataHash. 231 Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 232 Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 233 Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid'); 234 235 // Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag. 236 Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 237 $softwareEnforcedFlags = $objects[6]; 238 Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 239 $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); 240 241 Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 242 $teeEnforcedFlags = $objects[6]; 243 Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); 244 $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); 245 } 246 247 /** 248 * @param Sequence $sequence Obvious 249 * 250 * @return void 251 * @throws \Assert\AssertionFailedException 252 * @since 4.2.0 253 */ 254 private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void 255 { 256 foreach ($sequence->getChildren() as $tag) { 257 Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag'); 258 259 /** 260 * @var ExplicitlyTaggedObject $tag It is silly that I have to do that for PHPCS to be happy. 261 */ 262 Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found'); 263 } 264 } 265 }
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 |