223 lines
8.7 KiB
PHP
223 lines
8.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package Joomla.Plugin
|
|
* @subpackage System.Webauthn
|
|
*
|
|
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
|
|
* @license MIT; see libraries/vendor/web-auth/webauthn-lib/LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Plugin\System\Webauthn\Hotfix;
|
|
|
|
use Assert\Assertion;
|
|
use CBOR\Decoder;
|
|
use CBOR\MapObject;
|
|
use CBOR\OtherObject\OtherObjectManager;
|
|
use CBOR\Tag\TagObjectManager;
|
|
use Cose\Key\Ec2Key;
|
|
use Webauthn\AttestationStatement\AttestationStatement;
|
|
use Webauthn\AttestationStatement\AttestationStatementSupport;
|
|
use Webauthn\AuthenticatorData;
|
|
use Webauthn\CertificateToolbox;
|
|
use Webauthn\MetadataService\MetadataStatementRepository;
|
|
use Webauthn\StringStream;
|
|
use Webauthn\TrustPath\CertificateTrustPath;
|
|
|
|
// phpcs:disable PSR1.Files.SideEffects
|
|
\defined('_JEXEC') or die;
|
|
// phpcs:enable PSR1.Files.SideEffects
|
|
|
|
/**
|
|
* We had to fork the key attestation support object from the WebAuthn server package to address an
|
|
* issue with PHP 8.
|
|
*
|
|
* We are currently using an older version of the WebAuthn library (2.x) which was written before
|
|
* PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of
|
|
* Joomla's Semantic Versioning promise.
|
|
*
|
|
* The FidoU2FAttestationStatementSupport class forces an assertion on the result of the
|
|
* openssl_pkey_get_public() function, assuming it will return a resource. However, starting with
|
|
* PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a
|
|
* result, you cannot use Android or FIDO U2F keys with WebAuthn.
|
|
*
|
|
* The assertion check is in a private method, therefore we have to fork both attestation support
|
|
* class to change the assertion. The assertion takes place through a third party library we cannot
|
|
* (and should not!) modify.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @deprecated 4.2 will be removed in 6.0
|
|
* Will be removed without replacement
|
|
* We will upgrade the WebAuthn library to version 3 or later and this will go away.
|
|
*/
|
|
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
|
|
{
|
|
/**
|
|
* @var Decoder
|
|
* @since 4.2.0
|
|
*/
|
|
private $decoder;
|
|
|
|
/**
|
|
* @var MetadataStatementRepository|null
|
|
* @since 4.2.0
|
|
*/
|
|
private $metadataStatementRepository;
|
|
|
|
/**
|
|
* @param Decoder|null $decoder Obvious
|
|
* @param MetadataStatementRepository|null $metadataStatementRepository Obvious
|
|
*
|
|
* @since 4.2.0
|
|
*/
|
|
public function __construct(
|
|
?Decoder $decoder = null,
|
|
?MetadataStatementRepository $metadataStatementRepository = null
|
|
) {
|
|
if ($decoder !== null) {
|
|
@trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED);
|
|
}
|
|
|
|
if ($metadataStatementRepository === null) {
|
|
@trigger_error(
|
|
'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.',
|
|
E_USER_DEPRECATED
|
|
);
|
|
}
|
|
|
|
$this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager());
|
|
$this->metadataStatementRepository = $metadataStatementRepository;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
* @since 4.2.0
|
|
*/
|
|
public function name(): string
|
|
{
|
|
return 'fido-u2f';
|
|
}
|
|
|
|
/**
|
|
* @param array $attestation Obvious
|
|
*
|
|
* @return AttestationStatement
|
|
* @throws \Assert\AssertionFailedException
|
|
*
|
|
* @since 4.2.0
|
|
*/
|
|
public function load(array $attestation): AttestationStatement
|
|
{
|
|
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
|
|
|
|
foreach (['sig', 'x5c'] as $key) {
|
|
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
|
|
}
|
|
|
|
$certificates = $attestation['attStmt']['x5c'];
|
|
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
|
|
Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.');
|
|
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
|
|
|
|
reset($certificates);
|
|
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
|
$this->checkCertificate($certificates[0]);
|
|
|
|
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
|
|
}
|
|
|
|
/**
|
|
* @param string $clientDataJSONHash Obvious
|
|
* @param AttestationStatement $attestationStatement Obvious
|
|
* @param AuthenticatorData $authenticatorData Obvious
|
|
*
|
|
* @return boolean
|
|
* @throws \Assert\AssertionFailedException
|
|
* @since 4.2.0
|
|
*/
|
|
public function isValid(
|
|
string $clientDataJSONHash,
|
|
AttestationStatement $attestationStatement,
|
|
AuthenticatorData $authenticatorData
|
|
): bool {
|
|
Assertion::eq(
|
|
$authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
|
|
'00000000-0000-0000-0000-000000000000',
|
|
'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"'
|
|
);
|
|
|
|
if ($this->metadataStatementRepository !== null) {
|
|
CertificateToolbox::checkAttestationMedata(
|
|
$attestationStatement,
|
|
$authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
|
|
[],
|
|
$this->metadataStatementRepository
|
|
);
|
|
}
|
|
|
|
$trustPath = $attestationStatement->getTrustPath();
|
|
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
|
|
$dataToVerify = "\0";
|
|
$dataToVerify .= $authenticatorData->getRpIdHash();
|
|
$dataToVerify .= $clientDataJSONHash;
|
|
$dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
|
|
$dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
|
|
|
|
return openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256) === 1;
|
|
}
|
|
|
|
/**
|
|
* @param string|null $publicKey Obvious
|
|
*
|
|
* @return string
|
|
* @throws \Assert\AssertionFailedException
|
|
* @since 4.2.0
|
|
*/
|
|
private function extractPublicKey(?string $publicKey): string
|
|
{
|
|
Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.');
|
|
|
|
$publicKeyStream = new StringStream($publicKey);
|
|
$coseKey = $this->decoder->decode($publicKeyStream);
|
|
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
|
|
$publicKeyStream->close();
|
|
Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
|
|
|
|
$coseKey = $coseKey->getNormalizedData();
|
|
$ec2Key = new Ec2Key($coseKey + [Ec2Key::TYPE => 2, Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256]);
|
|
|
|
return "\x04" . $ec2Key->x() . $ec2Key->y();
|
|
}
|
|
|
|
/**
|
|
* @param string $publicKey Obvious
|
|
*
|
|
* @return void
|
|
* @throws \Assert\AssertionFailedException
|
|
* @since 4.2.0
|
|
*/
|
|
private function checkCertificate(string $publicKey): void
|
|
{
|
|
try {
|
|
$resource = openssl_pkey_get_public($publicKey);
|
|
|
|
if (version_compare(PHP_VERSION, '8.0', 'lt')) {
|
|
Assertion::isResource($resource, 'Unable to read the certificate');
|
|
} else {
|
|
/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
|
|
Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate');
|
|
}
|
|
} catch (\Throwable $throwable) {
|
|
throw new \InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable);
|
|
}
|
|
|
|
$details = openssl_pkey_get_details($resource);
|
|
Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain');
|
|
Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain');
|
|
Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain');
|
|
Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain');
|
|
Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain');
|
|
}
|
|
}
|