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