Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 09:35:33 +01:00
parent bb4b0f9be8
commit 011b183e28
4263 changed files with 3014 additions and 720369 deletions

View file

@ -1,147 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use function count;
use FG\ASN1\ASNObject;
use FG\ASN1\ExplicitlyTaggedObject;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use function Safe\hex2bin;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'android-key';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['sig', 'x5c', 'alg'] 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 at least one certificate.');
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
$signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
$alg = $attestationStatement->get('alg');
return 1 === openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg));
}
private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void
{
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
Assertion::isArray($details, 'Unable to read the certificate');
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($publicKeyData, 'No attested public key found');
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey);
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions
Assertion::isArray($certDetails, 'The certificate is not valid');
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
Assertion::keyExists($certDetails['extensions'], '1.3.6.1.4.1.11129.2.1.17', 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing');
$extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
$extensionAsAsn1 = ASNObject::fromBinary($extension);
Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$objects = $extensionAsAsn1->getChildren();
//Check that attestationChallenge is set to the clientDataHash.
Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid');
//Check that both teeEnforced and softwareEnforced structures dont contain allApplications(600) tag.
Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$softwareEnforcedFlags = $objects[6];
Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$teeEnforcedFlags = $objects[6];
Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
}
private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
{
foreach ($sequence->getChildren() as $tag) {
Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag');
/* @var ExplicitlyTaggedObject $tag */
Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found');
}
}
}

View file

@ -1,292 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm as AlgorithmInterface;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use function Safe\json_decode;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\TrustPath\CertificateTrustPath;
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var string|null
*/
private $apiKey;
/**
* @var ClientInterface|null
*/
private $client;
/**
* @var CompactSerializer
*/
private $jwsSerializer;
/**
* @var JWSVerifier|null
*/
private $jwsVerifier;
/**
* @var RequestFactoryInterface|null
*/
private $requestFactory;
/**
* @var int
*/
private $leeway;
/**
* @var int
*/
private $maxAge;
public function __construct(?ClientInterface $client = null, ?string $apiKey = null, ?RequestFactoryInterface $requestFactory = null, ?int $leeway = null, ?int $maxAge = null)
{
if (!class_exists(Algorithm\RS256::class)) {
throw new RuntimeException('The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?');
}
if (!class_exists(JWKFactory::class)) {
throw new RuntimeException('The class Jose\Component\KeyManagement\JWKFactory is missing. Did you forget to install the package web-token/jwt-key-mgmt?');
}
if (null !== $client) {
@trigger_error('The argument "client" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $apiKey) {
@trigger_error('The argument "apiKey" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $requestFactory) {
@trigger_error('The argument "requestFactory" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $maxAge) {
@trigger_error('The argument "maxAge" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setMaxAge".', E_USER_DEPRECATED);
}
if (null !== $leeway) {
@trigger_error('The argument "leeway" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setLeeway".', E_USER_DEPRECATED);
}
$this->jwsSerializer = new CompactSerializer();
$this->initJwsVerifier();
//To be removed in 4.0
$this->leeway = $leeway ?? 0;
$this->maxAge = $maxAge ?? 60000;
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
}
public function enableApiVerification(ClientInterface $client, string $apiKey, RequestFactoryInterface $requestFactory): self
{
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
return $this;
}
public function setMaxAge(int $maxAge): self
{
$this->maxAge = $maxAge;
return $this;
}
public function setLeeway(int $leeway): self
{
$this->leeway = $leeway;
return $this;
}
public function name(): string
{
return 'android-safetynet';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['ver', 'response'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
Assertion::notEmpty($attestation['attStmt'][$key], sprintf('The attestation statement value "%s" is empty.', $key));
}
$jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
$jwsHeader = $jws->getSignature(0)->getProtectedHeader();
Assertion::keyExists($jwsHeader, 'x5c', 'The response in the attestation statement must contain a "x5c" header.');
Assertion::notEmpty($jwsHeader['x5c'], 'The "x5c" parameter in the attestation statement response must contain at least one certificate.');
$certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
$attestation['attStmt']['jws'] = $jws;
return AttestationStatement::createBasic(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
$firstCertificate = current($certificates);
Assertion::string($firstCertificate, 'No certificate');
$parsedCertificate = openssl_x509_parse($firstCertificate);
Assertion::isArray($parsedCertificate, 'Invalid attestation object');
Assertion::keyExists($parsedCertificate, 'subject', 'Invalid attestation object');
Assertion::keyExists($parsedCertificate['subject'], 'CN', 'Invalid attestation object');
Assertion::eq($parsedCertificate['subject']['CN'], 'attest.android.com', 'Invalid attestation object');
/** @var JWS $jws */
$jws = $attestationStatement->get('jws');
$payload = $jws->getPayload();
$this->validatePayload($payload, $clientDataJSONHash, $authenticatorData);
//Check the signature
$this->validateSignature($jws, $trustPath);
//Check against Google service
$this->validateUsingGoogleApi($attestationStatement);
return true;
}
private function validatePayload(?string $payload, string $clientDataJSONHash, AuthenticatorData $authenticatorData): void
{
Assertion::notNull($payload, 'Invalid attestation object');
$payload = JsonConverter::decode($payload);
Assertion::isArray($payload, 'Invalid attestation object');
Assertion::keyExists($payload, 'nonce', 'Invalid attestation object. "nonce" is missing.');
Assertion::eq($payload['nonce'], base64_encode(hash('sha256', $authenticatorData->getAuthData().$clientDataJSONHash, true)), 'Invalid attestation object. Invalid nonce');
Assertion::keyExists($payload, 'ctsProfileMatch', 'Invalid attestation object. "ctsProfileMatch" is missing.');
Assertion::true($payload['ctsProfileMatch'], 'Invalid attestation object. "ctsProfileMatch" value is false.');
Assertion::keyExists($payload, 'timestampMs', 'Invalid attestation object. Timestamp is missing.');
Assertion::integer($payload['timestampMs'], 'Invalid attestation object. Timestamp shall be an integer.');
$currentTime = time() * 1000;
Assertion::lessOrEqualThan($payload['timestampMs'], $currentTime + $this->leeway, sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
Assertion::lessOrEqualThan($currentTime - $payload['timestampMs'], $this->maxAge, sprintf('Invalid attestation object. Too old. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
}
private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void
{
$jwk = JWKFactory::createFromCertificate($trustPath->getCertificates()[0]);
$isValid = $this->jwsVerifier->verifyWithKey($jws, $jwk, 0);
Assertion::true($isValid, 'Invalid response signature');
}
private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
{
if (null === $this->client || null === $this->apiKey || null === $this->requestFactory) {
return;
}
$uri = sprintf('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', urlencode($this->apiKey));
$requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
$request = $this->requestFactory->createRequest('POST', $uri);
$request = $request->withHeader('content-type', 'application/json');
$request->getBody()->write($requestBody);
$response = $this->client->sendRequest($request);
$this->checkGoogleApiResponse($response);
$responseBody = $this->getResponseBody($response);
$responseBodyJson = json_decode($responseBody, true);
Assertion::keyExists($responseBodyJson, 'isValidSignature', 'Invalid response.');
Assertion::boolean($responseBodyJson['isValidSignature'], 'Invalid response.');
Assertion::true($responseBodyJson['isValidSignature'], 'Invalid response.');
}
private function getResponseBody(ResponseInterface $response): string
{
$responseBody = '';
$response->getBody()->rewind();
while (true) {
$tmp = $response->getBody()->read(1024);
if ('' === $tmp) {
break;
}
$responseBody .= $tmp;
}
return $responseBody;
}
private function checkGoogleApiResponse(ResponseInterface $response): void
{
Assertion::eq(200, $response->getStatusCode(), 'Request did not succeeded');
Assertion::true($response->hasHeader('content-type'), 'Unrecognized response');
foreach ($response->getHeader('content-type') as $header) {
if (0 === mb_strpos($header, 'application/json')) {
return;
}
}
throw new InvalidArgumentException('Unrecognized response');
}
/**
* @param string[] $certificates
*
* @return string[]
*/
private function convertCertificatesToPem(array $certificates): array
{
foreach ($certificates as $k => $v) {
$certificates[$k] = CertificateToolbox::fixPEMStructure($v);
}
return $certificates;
}
private function initJwsVerifier(): void
{
$algorithmClasses = [
Algorithm\RS256::class, Algorithm\RS384::class, Algorithm\RS512::class,
Algorithm\PS256::class, Algorithm\PS384::class, Algorithm\PS512::class,
Algorithm\ES256::class, Algorithm\ES384::class, Algorithm\ES512::class,
Algorithm\EdDSA::class,
];
/* @var AlgorithmInterface[] $algorithms */
$algorithms = [];
foreach ($algorithmClasses as $algorithm) {
if (class_exists($algorithm)) {
/* @var AlgorithmInterface $algorithm */
$algorithms[] = new $algorithm();
}
}
$algorithmManager = new AlgorithmManager($algorithms);
$this->jwsVerifier = new JWSVerifier($algorithmManager);
}
}

View file

@ -1,119 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use function count;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class AppleAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'apple';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['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 at least one certificate.');
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createAnonymizationCA($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
return true;
}
private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void
{
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
Assertion::isArray($details, 'Unable to read the certificate');
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($publicKeyData, 'No attested public key found');
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey);
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
//We check the attested key corresponds to the key in the certificate
Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Apple Extension with OID “1.2.840.113635.100.8.2” in certificate extensions
Assertion::isArray($certDetails, 'The certificate is not valid');
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
Assertion::keyExists($certDetails['extensions'], '1.2.840.113635.100.8.2', 'The certificate extension "1.2.840.113635.100.8.2" is missing');
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
$nonceToHash = $authenticatorData->getAuthData().$clientDataHash;
$nonce = hash('sha256', $nonceToHash);
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
Assertion::eq('3024a1220420'.$nonce, bin2hex($extension), 'The client data hash is not valid');
}
}

View file

@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
use Webauthn\MetadataService\MetadataStatement;
class AttestationObject
{
/**
* @var string
*/
private $rawAttestationObject;
/**
* @var AttestationStatement
*/
private $attStmt;
/**
* @var AuthenticatorData
*/
private $authData;
/**
* @var MetadataStatement|null
*/
private $metadataStatement;
public function __construct(string $rawAttestationObject, AttestationStatement $attStmt, AuthenticatorData $authData, ?MetadataStatement $metadataStatement = null)
{
if (null !== $metadataStatement) {
@trigger_error('The argument "metadataStatement" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatement".', E_USER_DEPRECATED);
}
$this->rawAttestationObject = $rawAttestationObject;
$this->attStmt = $attStmt;
$this->authData = $authData;
$this->metadataStatement = $metadataStatement;
}
public function getRawAttestationObject(): string
{
return $this->rawAttestationObject;
}
public function getAttStmt(): AttestationStatement
{
return $this->attStmt;
}
public function setAttStmt(AttestationStatement $attStmt): void
{
$this->attStmt = $attStmt;
}
public function getAuthData(): AuthenticatorData
{
return $this->authData;
}
public function getMetadataStatement(): ?MetadataStatement
{
return $this->metadataStatement;
}
public function setMetadataStatement(MetadataStatement $metadataStatement): self
{
$this->metadataStatement = $metadataStatement;
return $this;
}
}

View file

@ -1,148 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use function ord;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\sprintf;
use function Safe\unpack;
use Throwable;
use Webauthn\AttestedCredentialData;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
use Webauthn\AuthenticatorData;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\StringStream;
class AttestationObjectLoader
{
private const FLAG_AT = 0b01000000;
private const FLAG_ED = 0b10000000;
/**
* @var Decoder
*/
private $decoder;
/**
* @var AttestationStatementSupportManager
*/
private $attestationStatementSupportManager;
/**
* @var LoggerInterface|null
*/
private $logger;
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
{
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.2 and will be removed in 4.0. Please set `null` instead.', E_USER_DEPRECATED);
}
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger" instead.', E_USER_DEPRECATED);
}
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
$this->logger = $logger ?? new NullLogger();
}
public static function create(AttestationStatementSupportManager $attestationStatementSupportManager): self
{
return new self($attestationStatementSupportManager);
}
public function load(string $data): AttestationObject
{
try {
$this->logger->info('Trying to load the data', ['data' => $data]);
$decodedData = Base64Url::decode($data);
$stream = new StringStream($decodedData);
$parsed = $this->decoder->decode($stream);
$this->logger->info('Loading the Attestation Statement');
$attestationObject = $parsed->getNormalizedData();
Assertion::true($stream->isEOF(), 'Invalid attestation object. Presence of extra bytes.');
$stream->close();
Assertion::isArray($attestationObject, 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'authData', 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'fmt', 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'attStmt', 'Invalid attestation object');
$authData = $attestationObject['authData'];
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
$attestationStatement = $attestationStatementSupport->load($attestationObject);
$this->logger->info('Attestation Statement loaded');
$this->logger->debug('Attestation Statement loaded', ['attestationStatement' => $attestationStatement]);
$authDataStream = new StringStream($authData);
$rp_id_hash = $authDataStream->read(32);
$flags = $authDataStream->read(1);
$signCount = $authDataStream->read(4);
$signCount = unpack('N', $signCount)[1];
$this->logger->debug(sprintf('Signature counter: %d', $signCount));
$attestedCredentialData = null;
if (0 !== (ord($flags) & self::FLAG_AT)) {
$this->logger->info('Attested Credential Data is present');
$aaguid = Uuid::fromBytes($authDataStream->read(16));
$credentialLength = $authDataStream->read(2);
$credentialLength = unpack('n', $credentialLength)[1];
$credentialId = $authDataStream->read($credentialLength);
$credentialPublicKey = $this->decoder->decode($authDataStream);
Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
$attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
$this->logger->info('Attested Credential Data loaded');
$this->logger->debug('Attested Credential Data loaded', ['at' => $attestedCredentialData]);
}
$extension = null;
if (0 !== (ord($flags) & self::FLAG_ED)) {
$this->logger->info('Extension Data loaded');
$extension = $this->decoder->decode($authDataStream);
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
$this->logger->info('Extension Data loaded');
$this->logger->debug('Extension Data loaded', ['ed' => $extension]);
}
Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
$authDataStream->close();
$authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
$attestationObject = new AttestationObject($data, $attestationStatement, $authenticatorData);
$this->logger->info('Attestation Object loaded');
$this->logger->debug('Attestation Object', ['ed' => $attestationObject]);
return $attestationObject;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
}

View file

@ -1,175 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
use Webauthn\TrustPath\TrustPath;
use Webauthn\TrustPath\TrustPathLoader;
class AttestationStatement implements JsonSerializable
{
public const TYPE_NONE = 'none';
public const TYPE_BASIC = 'basic';
public const TYPE_SELF = 'self';
public const TYPE_ATTCA = 'attca';
public const TYPE_ECDAA = 'ecdaa';
public const TYPE_ANONCA = 'anonca';
/**
* @var string
*/
private $fmt;
/**
* @var mixed[]
*/
private $attStmt;
/**
* @var TrustPath
*/
private $trustPath;
/**
* @var string
*/
private $type;
/**
* @param mixed[] $attStmt
*/
public function __construct(string $fmt, array $attStmt, string $type, TrustPath $trustPath)
{
$this->fmt = $fmt;
$this->attStmt = $attStmt;
$this->type = $type;
$this->trustPath = $trustPath;
}
/**
* @param mixed[] $attStmt
*/
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_NONE, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_SELF, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ATTCA, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
}
public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ANONCA, $trustPath);
}
public function getFmt(): string
{
return $this->fmt;
}
/**
* @return mixed[]
*/
public function getAttStmt(): array
{
return $this->attStmt;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->attStmt);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The attestation statement has no key "%s".', $key));
return $this->attStmt[$key];
}
public function getTrustPath(): TrustPath
{
return $this->trustPath;
}
public function getType(): string
{
return $this->type;
}
/**
* @param mixed[] $data
*/
public static function createFromArray(array $data): self
{
foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) {
Assertion::keyExists($data, $key, sprintf('The key "%s" is missing', $key));
}
return new self(
$data['fmt'],
$data['attStmt'],
$data['type'],
TrustPathLoader::loadTrustPath($data['trustPath'])
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
return [
'fmt' => $this->fmt,
'attStmt' => $this->attStmt,
'trustPath' => $this->trustPath->jsonSerialize(),
'type' => $this->type,
];
}
}

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
interface AttestationStatementSupport
{
public function name(): string;
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement;
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool;
}

View file

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use function Safe\sprintf;
class AttestationStatementSupportManager
{
/**
* @var AttestationStatementSupport[]
*/
private $attestationStatementSupports = [];
public function add(AttestationStatementSupport $attestationStatementSupport): void
{
$this->attestationStatementSupports[$attestationStatementSupport->name()] = $attestationStatementSupport;
}
public function has(string $name): bool
{
return array_key_exists($name, $this->attestationStatementSupports);
}
public function get(string $name): AttestationStatementSupport
{
Assertion::true($this->has($name), sprintf('The attestation statement format "%s" is not supported.', $name));
return $this->attestationStatementSupports[$name];
}
}

View file

@ -1,118 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Key\Ec2Key;
use InvalidArgumentException;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Throwable;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'fido-u2f';
}
/**
* @param mixed[] $attestation
*/
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));
}
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"'
);
$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 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256);
}
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();
}
private function checkCertificate(string $publicKey): void
{
try {
$resource = openssl_pkey_get_public($publicKey);
$details = openssl_pkey_get_details($resource);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable);
}
Assertion::isArray($details, 'Invalid certificate or certificate chain');
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');
}
}

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use function count;
use Webauthn\AuthenticatorData;
use Webauthn\TrustPath\EmptyTrustPath;
final class NoneAttestationStatementSupport implements AttestationStatementSupport
{
public function name(): string
{
return 'none';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::noContent($attestation['attStmt'], 'Invalid attestation object');
return AttestationStatement::createNone($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
return 0 === count($attestationStatement->getAttStmt());
}
}

View file

@ -1,194 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Algorithms;
use Cose\Key\Key;
use function in_array;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
use Webauthn\Util\CoseSignatureFixer;
final class PackedAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
/**
* @var Manager
*/
private $algorithmManager;
public function __construct(Manager $algorithmManager)
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->algorithmManager = $algorithmManager;
}
public function name(): string
{
return 'packed';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation['attStmt'], 'sig', 'The attestation statement value "sig" is missing.');
Assertion::keyExists($attestation['attStmt'], 'alg', 'The attestation statement value "alg" is missing.');
Assertion::string($attestation['attStmt']['sig'], 'The attestation statement value "sig" is missing.');
switch (true) {
case array_key_exists('x5c', $attestation['attStmt']):
return $this->loadBasicType($attestation);
case array_key_exists('ecdaaKeyId', $attestation['attStmt']):
return $this->loadEcdaaType($attestation['attStmt']);
default:
return $this->loadEmptyType($attestation);
}
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
switch (true) {
case $trustPath instanceof CertificateTrustPath:
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData, $trustPath);
case $trustPath instanceof EcdaaKeyIdTrustPath:
return $this->processWithECDAA();
case $trustPath instanceof EmptyTrustPath:
return $this->processWithSelfAttestation($clientDataJSONHash, $attestationStatement, $authenticatorData);
default:
throw new InvalidArgumentException('Unsupported attestation statement');
}
}
/**
* @param mixed[] $attestation
*/
private function loadBasicType(array $attestation): AttestationStatement
{
$certificates = $attestation['attStmt']['x5c'];
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
private function loadEcdaaType(array $attestation): AttestationStatement
{
$ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId'];
Assertion::string($ecdaaKeyId, 'The attestation statement value "ecdaaKeyId" is invalid.');
return AttestationStatement::createEcdaa($attestation['fmt'], $attestation['attStmt'], new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId']));
}
/**
* @param mixed[] $attestation
*/
private function loadEmptyType(array $attestation): AttestationStatement
{
return AttestationStatement::createSelf($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
Assertion::isArray($parsed, 'Invalid certificate');
//Check version
Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
//Check subject field
Assertion::false(!isset($parsed['name']) || false === mb_strpos($parsed['name'], '/OU=Authenticator Attestation'), 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"');
//Check extensions
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
//Check certificate is not a CA cert
Assertion::false(!isset($parsed['extensions']['basicConstraints']) || 'CA:FALSE' !== $parsed['extensions']['basicConstraints'], 'The Basic Constraints extension must have the CA component set to false');
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential available');
// id-fido-gen-ce-aaguid OID check
Assertion::false(in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($attestedCredentialData->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');
}
private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData, CertificateTrustPath $trustPath): bool
{
$certificates = $trustPath->getCertificates();
// Check leaf certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
// Verification of the signature
$signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
$result = openssl_verify($signedData, $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
return 1 === $result;
}
private function processWithECDAA(): bool
{
throw new RuntimeException('ECDAA not supported');
}
private function processWithSelfAttestation(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential available');
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($credentialPublicKey, 'No credential public key available');
$publicKeyStream = new StringStream($credentialPublicKey);
$publicKey = $this->decoder->decode($publicKeyStream);
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
$publicKeyStream->close();
Assertion::isInstanceOf($publicKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
$publicKey = $publicKey->getNormalizedData(false);
$publicKey = new Key($publicKey);
Assertion::eq($publicKey->alg(), (int) $attestationStatement->get('alg'), 'The algorithm of the attestation statement and the key are not identical.');
$dataToVerify = $authenticatorData->getAuthData().$clientDataJSONHash;
$algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg'));
if (!$algorithm instanceof Signature) {
throw new RuntimeException('Invalid algorithm');
}
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
return $algorithm->verify($dataToVerify, $publicKey, $signature);
}
}

View file

@ -1,309 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\OkpKey;
use Cose\Key\RsaKey;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use Safe\DateTimeImmutable;
use function Safe\sprintf;
use function Safe\unpack;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
final class TPMAttestationStatementSupport implements AttestationStatementSupport
{
public function name(): string
{
return 'tpm';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported');
foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object');
$certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object');
$pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
$pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit');
$pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true);
$attestedName = $pubAreaAttestedNameAlg.$pubAreaHash;
Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name');
$attestation['attStmt']['parsedCertInfo'] = $certInfo;
$attestation['attStmt']['parsedPubArea'] = $pubArea;
$certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
return AttestationStatement::createAttCA(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash;
$attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true);
Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash');
$this->checkUniquePublicKey(
$attestationStatement->get('parsedPubArea')['unique'],
$authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()
);
switch (true) {
case $attestationStatement->getTrustPath() instanceof CertificateTrustPath:
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath:
return $this->processWithECDAA();
default:
throw new InvalidArgumentException('Unsupported attestation statement');
}
}
private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
{
$cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key');
$key = new Key($publicKey->getNormalizedData(false));
switch ($key->type()) {
case Key::TYPE_OKP:
$uniqueFromKey = (new OkpKey($key->getData()))->x();
break;
case Key::TYPE_EC2:
$ec2Key = new Ec2Key($key->getData());
$uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y();
break;
case Key::TYPE_RSA:
$uniqueFromKey = (new RsaKey($key->getData()))->n();
break;
default:
throw new InvalidArgumentException('Invalid or unsupported key type.');
}
Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value');
}
/**
* @return mixed[]
*/
private function checkCertInfo(string $data): array
{
$certInfo = new StringStream($data);
$magic = $certInfo->read(4);
Assertion::eq('ff544347', bin2hex($magic), 'Invalid attestation object');
$type = $certInfo->read(2);
$qualifiedSignerLength = unpack('n', $certInfo->read(2))[1];
$qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored
$extraDataLength = unpack('n', $certInfo->read(2))[1];
$extraData = $certInfo->read($extraDataLength);
$clockInfo = $certInfo->read(17); //Ignore
$firmwareVersion = $certInfo->read(8);
$attestedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedName = $certInfo->read($attestedNameLength);
$attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore
Assertion::true($certInfo->isEOF(), 'Invalid certificate information. Presence of extra bytes.');
$certInfo->close();
return [
'magic' => $magic,
'type' => $type,
'qualifiedSigner' => $qualifiedSigner,
'extraData' => $extraData,
'clockInfo' => $clockInfo,
'firmwareVersion' => $firmwareVersion,
'attestedName' => $attestedName,
'attestedQualifiedName' => $attestedQualifiedName,
];
}
/**
* @return mixed[]
*/
private function checkPubArea(string $data): array
{
$pubArea = new StringStream($data);
$type = $pubArea->read(2);
$nameAlg = $pubArea->read(2);
$objectAttributes = $pubArea->read(4);
$authPolicyLength = unpack('n', $pubArea->read(2))[1];
$authPolicy = $pubArea->read($authPolicyLength);
$parameters = $this->getParameters($type, $pubArea);
$uniqueLength = unpack('n', $pubArea->read(2))[1];
$unique = $pubArea->read($uniqueLength);
Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.');
$pubArea->close();
return [
'type' => $type,
'nameAlg' => $nameAlg,
'objectAttributes' => $objectAttributes,
'authPolicy' => $authPolicy,
'parameters' => $parameters,
'unique' => $unique,
];
}
/**
* @return mixed[]
*/
private function getParameters(string $type, StringStream $stream): array
{
switch (bin2hex($type)) {
case '0001':
case '0014':
case '0016':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'keyBits' => unpack('n', $stream->read(2))[1],
'exponent' => $this->getExponent($stream->read(4)),
];
case '0018':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'curveId' => $stream->read(2),
'kdf' => $stream->read(2),
];
default:
throw new InvalidArgumentException('Unsupported type');
}
}
private function getExponent(string $exponent): string
{
return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent;
}
private function getTPMHash(string $nameAlg): string
{
switch (bin2hex($nameAlg)) {
case '0004':
return 'sha1'; //: "TPM_ALG_SHA1",
case '000b':
return 'sha256'; //: "TPM_ALG_SHA256",
case '000c':
return 'sha384'; //: "TPM_ALG_SHA384",
case '000d':
return 'sha512'; //: "TPM_ALG_SHA512",
default:
throw new InvalidArgumentException('Unsupported hash algorithm');
}
}
private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
// Check certificate CA chain and returns the Attestation Certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
$result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
return 1 === $result;
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
Assertion::isArray($parsed, 'Invalid certificate');
//Check version
Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
//Check subject field is empty
Assertion::false(!isset($parsed['subject']) || !is_array($parsed['subject']) || 0 !== count($parsed['subject']), 'Invalid certificate name. The Subject should be empty');
// Check period of validity
Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.');
Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.');
$startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.');
Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.');
Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.');
$endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.');
//Check extensions
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
//Check subjectAltName
Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing');
//Check extendedKeyUsage
Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing');
Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid');
// id-fido-gen-ce-aaguid OID check
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');
}
private function processWithECDAA(): bool
{
throw new RuntimeException('ECDAA not supported');
}
}

View file

@ -1,113 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use JsonSerializable;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function Safe\base64_decode;
/**
* @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data
*/
class AttestedCredentialData implements JsonSerializable
{
/**
* @var UuidInterface
*/
private $aaguid;
/**
* @var string
*/
private $credentialId;
/**
* @var string|null
*/
private $credentialPublicKey;
public function __construct(UuidInterface $aaguid, string $credentialId, ?string $credentialPublicKey)
{
$this->aaguid = $aaguid;
$this->credentialId = $credentialId;
$this->credentialPublicKey = $credentialPublicKey;
}
public function getAaguid(): UuidInterface
{
return $this->aaguid;
}
public function setAaguid(UuidInterface $aaguid): void
{
$this->aaguid = $aaguid;
}
public function getCredentialId(): string
{
return $this->credentialId;
}
public function getCredentialPublicKey(): ?string
{
return $this->credentialPublicKey;
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'aaguid', 'Invalid input. "aaguid" is missing.');
Assertion::keyExists($json, 'credentialId', 'Invalid input. "credentialId" is missing.');
switch (true) {
case 36 === mb_strlen($json['aaguid'], '8bit'):
$uuid = Uuid::fromString($json['aaguid']);
break;
default: // Kept for compatibility with old format
$decoded = base64_decode($json['aaguid'], true);
$uuid = Uuid::fromBytes($decoded);
}
$credentialId = base64_decode($json['credentialId'], true);
$credentialPublicKey = null;
if (isset($json['credentialPublicKey'])) {
$credentialPublicKey = base64_decode($json['credentialPublicKey'], true);
}
return new self(
$uuid,
$credentialId,
$credentialPublicKey
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$result = [
'aaguid' => $this->aaguid->toString(),
'credentialId' => base64_encode($this->credentialId),
];
if (null !== $this->credentialPublicKey) {
$result['credentialPublicKey'] = base64_encode($this->credentialPublicKey);
}
return $result;
}
}

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use JsonSerializable;
class AuthenticationExtension implements JsonSerializable
{
/**
* @var string
*/
private $name;
/**
* @var mixed
*/
private $value;
/**
* @param mixed $value
*/
public function __construct(string $name, $value)
{
$this->name = $name;
$this->value = $value;
}
public function name(): string
{
return $this->name;
}
/**
* @return mixed
*/
public function value()
{
return $this->value;
}
/**
* @return mixed
*/
public function jsonSerialize()
{
return $this->value;
}
}

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use function array_key_exists;
use ArrayIterator;
use Assert\Assertion;
use function count;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use function Safe\sprintf;
class AuthenticationExtensionsClientInputs implements JsonSerializable, Countable, IteratorAggregate
{
/**
* @var AuthenticationExtension[]
*/
private $extensions = [];
public function add(AuthenticationExtension $extension): void
{
$this->extensions[$extension->name()] = $extension;
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
$object = new self();
foreach ($json as $k => $v) {
$object->add(new AuthenticationExtension($k, $v));
}
return $object;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->extensions);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key));
return $this->extensions[$key];
}
/**
* @return AuthenticationExtension[]
*/
public function jsonSerialize(): array
{
return array_map(static function (AuthenticationExtension $object) {
return $object->jsonSerialize();
}, $this->extensions);
}
/**
* @return Iterator<string, AuthenticationExtension>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->extensions);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->extensions, $mode);
}
}

View file

@ -1,97 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use function array_key_exists;
use ArrayIterator;
use Assert\Assertion;
use function count;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use function Safe\json_decode;
use function Safe\sprintf;
class AuthenticationExtensionsClientOutputs implements JsonSerializable, Countable, IteratorAggregate
{
/**
* @var AuthenticationExtension[]
*/
private $extensions = [];
public function add(AuthenticationExtension $extension): void
{
$this->extensions[$extension->name()] = $extension;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
$object = new self();
foreach ($json as $k => $v) {
$object->add(new AuthenticationExtension($k, $v));
}
return $object;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->extensions);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key));
return $this->extensions[$key];
}
/**
* @return AuthenticationExtension[]
*/
public function jsonSerialize(): array
{
return array_map(static function (AuthenticationExtension $object) {
return $object->jsonSerialize();
}, $this->extensions);
}
/**
* @return Iterator<string, AuthenticationExtension>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->extensions);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->extensions, $mode);
}
}

View file

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use Assert\Assertion;
use CBOR\CBORObject;
use CBOR\MapObject;
abstract class AuthenticationExtensionsClientOutputsLoader
{
public static function load(CBORObject $object): AuthenticationExtensionsClientOutputs
{
Assertion::isInstanceOf($object, MapObject::class, 'Invalid extension object');
$data = $object->getNormalizedData();
$extensions = new AuthenticationExtensionsClientOutputs();
foreach ($data as $key => $value) {
Assertion::string($key, 'Invalid extension key');
$extensions->add(new AuthenticationExtension($key, $value));
}
return $extensions;
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
interface ExtensionOutputChecker
{
/**
* @throws ExtensionOutputError
*/
public function check(AuthenticationExtensionsClientInputs $inputs, AuthenticationExtensionsClientOutputs $outputs): void;
}

View file

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
class ExtensionOutputCheckerHandler
{
/**
* @var ExtensionOutputChecker[]
*/
private $checkers = [];
public function add(ExtensionOutputChecker $checker): void
{
$this->checkers[] = $checker;
}
/**
* @throws ExtensionOutputError
*/
public function check(AuthenticationExtensionsClientInputs $inputs, AuthenticationExtensionsClientOutputs $outputs): void
{
foreach ($this->checkers as $checker) {
$checker->check($inputs, $outputs);
}
}
}

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use Exception;
use Throwable;
class ExtensionOutputError extends Exception
{
/**
* @var AuthenticationExtension
*/
private $authenticationExtension;
public function __construct(AuthenticationExtension $authenticationExtension, string $message = '', int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->authenticationExtension = $authenticationExtension;
}
public function getAuthenticationExtension(): AuthenticationExtension
{
return $this->authenticationExtension;
}
}

View file

@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function Safe\base64_decode;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
*/
class AuthenticatorAssertionResponse extends AuthenticatorResponse
{
/**
* @var AuthenticatorData
*/
private $authenticatorData;
/**
* @var string
*/
private $signature;
/**
* @var string|null
*/
private $userHandle;
public function __construct(CollectedClientData $clientDataJSON, AuthenticatorData $authenticatorData, string $signature, ?string $userHandle)
{
parent::__construct($clientDataJSON);
$this->authenticatorData = $authenticatorData;
$this->signature = $signature;
$this->userHandle = $userHandle;
}
public function getAuthenticatorData(): AuthenticatorData
{
return $this->authenticatorData;
}
public function getSignature(): string
{
return $this->signature;
}
public function getUserHandle(): ?string
{
if (null === $this->userHandle || '' === $this->userHandle) {
return $this->userHandle;
}
return base64_decode($this->userHandle, true);
}
}

View file

@ -1,272 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Key;
use function count;
use function in_array;
use function is_string;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use function Safe\parse_url;
use Throwable;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\Counter\CounterChecker;
use Webauthn\Counter\ThrowExceptionIfInvalid;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\Util\CoseSignatureFixer;
class AuthenticatorAssertionResponseValidator
{
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSourceRepository;
/**
* @var Decoder
*/
private $decoder;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var Manager|null
*/
private $algorithmManager;
/**
* @var CounterChecker
*/
private $counterChecker;
/**
* @var LoggerInterface|null
*/
private $logger;
public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, Manager $algorithmManager, ?CounterChecker $counterChecker = null, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
if (null !== $counterChecker) {
@trigger_error('The argument "counterChecker" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setCounterChecker".', E_USER_DEPRECATED);
}
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->algorithmManager = $algorithmManager;
$this->counterChecker = $counterChecker ?? new ThrowExceptionIfInvalid();
$this->logger = $logger ?? new NullLogger();
}
/**
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
*/
public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface $request, ?string $userHandle, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator assertion response', [
'credentialId' => $credentialId,
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
'host' => $request->getUri()->getHost(),
'userHandle' => $userHandle,
]);
/** @see 7.2.1 */
if (0 !== count($publicKeyCredentialRequestOptions->getAllowCredentials())) {
Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.');
}
/** @see 7.2.2 */
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($credentialId);
Assertion::notNull($publicKeyCredentialSource, 'The credential ID is invalid.');
/** @see 7.2.3 */
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
$credentialUserHandle = $publicKeyCredentialSource->getUserHandle();
$responseUserHandle = $authenticatorAssertionResponse->getUserHandle();
/** @see 7.2.2 User Handle*/
if (null !== $userHandle) { //If the user was identified before the authentication ceremony was initiated,
Assertion::eq($credentialUserHandle, $userHandle, 'Invalid user handle');
if (null !== $responseUserHandle && '' !== $responseUserHandle) {
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
} else {
Assertion::notEmpty($responseUserHandle, 'User handle is mandatory');
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
if ($isU2F) {
$credentialPublicKey = U2FPublicKey::createCOSEKey($credentialPublicKey);
}
Assertion::notNull($credentialPublicKey, 'No public key available.');
$stream = new StringStream($credentialPublicKey);
$credentialPublicKeyStream = $this->decoder->decode($stream);
Assertion::true($stream->isEOF(), 'Invalid key. Presence of extra bytes.');
$stream->close();
/** @see 7.2.4 */
/** @see 7.2.5 */
//Nothing to do. Use of objects directly
/** @see 7.2.6 */
$C = $authenticatorAssertionResponse->getClientDataJSON();
/** @see 7.2.7 */
Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".');
/** @see 7.2.8 */
Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.2.9 */
$rpId = $publicKeyCredentialRequestOptions->getRpId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialRequestOptions->getExtensions(), $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, 'Invalid origin');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
/** @see 7.2.10 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
$expectedRpIdHash = $isU2F ? $C->getOrigin() : $facetId;
// u2f response has full origin in rpIdHash
/** @see 7.2.11 */
$rpIdHash = hash('sha256', $expectedRpIdHash, true);
Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.2.12 */
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present');
/** @see 7.2.13 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification()) {
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.2.14 */
$extensionsClientOutputs = $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialRequestOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.2.15 */
$getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.2.16 */
$dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash;
$signature = $authenticatorAssertionResponse->getSignature();
$coseKey = new Key($credentialPublicKeyStream->getNormalizedData());
$algorithm = $this->algorithmManager->get($coseKey->alg());
Assertion::isInstanceOf($algorithm, Signature::class, 'Invalid algorithm identifier. Should refer to a signature algorithm');
$signature = CoseSignatureFixer::fix($signature, $algorithm);
Assertion::true($algorithm->verify($dataToVerify, $coseKey, $signature), 'Invalid signature.');
/** @see 7.2.17 */
$storedCounter = $publicKeyCredentialSource->getCounter();
$responseCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount();
if (0 !== $responseCounter || 0 !== $storedCounter) {
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
}
$publicKeyCredentialSource->setCounter($responseCounter);
$this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
/** @see 7.2.18 */
//All good. We can continue.
$this->logger->info('The assertion is valid');
$this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCounterChecker(CounterChecker $counterChecker): self
{
$this->counterChecker = $counterChecker;
return $this;
}
/**
* @param array<PublicKeyCredentialDescriptor> $allowedCredentials
*/
private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool
{
foreach ($allowedCredentials as $allowedCredential) {
if (hash_equals($allowedCredential->getId(), $credentialId)) {
return true;
}
}
return false;
}
private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
{
if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')->value();
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
if (!is_string($appId) || true !== $wasUsed) {
return $rpId;
}
return $appId;
}
}

View file

@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Webauthn\AttestationStatement\AttestationObject;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
*/
class AuthenticatorAttestationResponse extends AuthenticatorResponse
{
/**
* @var AttestationObject
*/
private $attestationObject;
public function __construct(CollectedClientData $clientDataJSON, AttestationObject $attestationObject)
{
parent::__construct($clientDataJSON);
$this->attestationObject = $attestationObject;
}
public function getAttestationObject(): AttestationObject
{
return $this->attestationObject;
}
}

View file

@ -1,384 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_string;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\parse_url;
use function Safe\sprintf;
use Throwable;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\CertificateChainChecker\CertificateChainChecker;
use Webauthn\MetadataService\MetadataStatement;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReport;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
class AuthenticatorAttestationResponseValidator
{
/**
* @var AttestationStatementSupportManager
*/
private $attestationStatementSupportManager;
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSource;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var MetadataStatementRepository|null
*/
private $metadataStatementRepository;
/**
* @var CertificateChainChecker|null
*/
private $certificateChainChecker;
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, PublicKeyCredentialSourceRepository $publicKeyCredentialSource, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatementRepository".', E_USER_DEPRECATED);
}
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
$this->publicKeyCredentialSource = $publicKeyCredentialSource;
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->metadataStatementRepository = $metadataStatementRepository;
$this->logger = $logger ?? new NullLogger();
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCertificateChainChecker(CertificateChainChecker $certificateChainChecker): self
{
$this->certificateChainChecker = $certificateChainChecker;
return $this;
}
public function setMetadataStatementRepository(MetadataStatementRepository $metadataStatementRepository): self
{
$this->metadataStatementRepository = $metadataStatementRepository;
return $this;
}
/**
* @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
*/
public function check(AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $request, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator attestation response', [
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
'host' => $request->getUri()->getHost(),
]);
/** @see 7.1.1 */
//Nothing to do
/** @see 7.1.2 */
$C = $authenticatorAttestationResponse->getClientDataJSON();
/** @see 7.1.3 */
Assertion::eq('webauthn.create', $C->getType(), 'The client data type is not "webauthn.create".');
/** @see 7.1.4 */
Assertion::true(hash_equals($publicKeyCredentialCreationOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.1.5 */
$rpId = $publicKeyCredentialCreationOptions->getRp()->getId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialCreationOptions->getExtensions(), $authenticatorAttestationResponse->getAttestationObject()->getAuthData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, sprintf('The origin URI "%s" is not valid', $C->getOrigin()));
Assertion::keyExists($parsedRelyingPartyId, 'scheme', 'Invalid origin rpId.');
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
/** @see 7.1.6 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
/** @see 7.1.7 */
$clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.1.8 */
$attestationObject = $authenticatorAttestationResponse->getAttestationObject();
/** @see 7.1.9 */
$rpIdHash = hash('sha256', $facetId, true);
Assertion::true(hash_equals($rpIdHash, $attestationObject->getAuthData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.1.10 */
Assertion::true($attestationObject->getAuthData()->isUserPresent(), 'User was not present');
/** @see 7.1.11 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialCreationOptions->getAuthenticatorSelection()->getUserVerification()) {
Assertion::true($attestationObject->getAuthData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.1.12 */
$extensionsClientOutputs = $attestationObject->getAuthData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialCreationOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.1.13 */
$this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject);
$fmt = $attestationObject->getAttStmt()->getFmt();
Assertion::true($this->attestationStatementSupportManager->has($fmt), 'Unsupported attestation statement format.');
/** @see 7.1.14 */
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
Assertion::true($attestationStatementSupport->isValid($clientDataJSONHash, $attestationObject->getAttStmt(), $attestationObject->getAuthData()), 'Invalid attestation statement.');
/** @see 7.1.15 */
/** @see 7.1.16 */
/** @see 7.1.17 */
Assertion::true($attestationObject->getAuthData()->hasAttestedCredentialData(), 'There is no attested credential data.');
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'There is no attested credential data.');
$credentialId = $attestedCredentialData->getCredentialId();
Assertion::null($this->publicKeyCredentialSource->findOneByCredentialId($credentialId), 'The credential ID already exists.');
/** @see 7.1.18 */
/** @see 7.1.19 */
$publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
$credentialId,
$attestedCredentialData,
$attestationObject,
$publicKeyCredentialCreationOptions->getUser()->getId()
);
$this->logger->info('The attestation is valid');
$this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
private function checkCertificateChain(AttestationStatement $attestationStatement, ?MetadataStatement $metadataStatement): void
{
$trustPath = $attestationStatement->getTrustPath();
if (!$trustPath instanceof CertificateTrustPath) {
return;
}
$authenticatorCertificates = $trustPath->getCertificates();
if (null === $metadataStatement) {
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, [], null);
return;
}
$metadataStatementCertificates = $metadataStatement->getAttestationRootCertificates();
$rootStatementCertificates = $metadataStatement->getRootCertificates();
foreach ($metadataStatementCertificates as $key => $metadataStatementCertificate) {
$metadataStatementCertificates[$key] = CertificateToolbox::fixPEMStructure($metadataStatementCertificate);
}
$trustedCertificates = array_merge(
$metadataStatementCertificates,
$rootStatementCertificates
);
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates, $trustedCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, $trustedCertificates);
}
private function checkMetadataStatement(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, AttestationObject $attestationObject): void
{
$attestationStatement = $attestationObject->getAttStmt();
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$aaguid = $attestedCredentialData->getAaguid()->toString();
if (PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE === $publicKeyCredentialCreationOptions->getAttestation()) {
$this->logger->debug('No attestation is asked.');
//No attestation is asked. We shall ensure that the data is anonymous.
if (
'00000000-0000-0000-0000-000000000000' === $aaguid
&& (AttestationStatement::TYPE_NONE === $attestationStatement->getType() || AttestationStatement::TYPE_SELF === $attestationStatement->getType())) {
$this->logger->debug('The Attestation Statement is anonymous.');
$this->checkCertificateChain($attestationStatement, null);
return;
}
$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
'aaguid' => $aaguid,
'AttestationStatement' => $attestationStatement,
]);
$attestedCredentialData->setAaguid(
Uuid::fromString('00000000-0000-0000-0000-000000000000')
);
$attestationObject->setAttStmt(AttestationStatement::createNone('none', [], new EmptyTrustPath()));
return;
}
if (AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
$this->logger->debug('No attestation returned.');
//No attestation is returned. We shall ensure that the AAGUID is a null one.
if ('00000000-0000-0000-0000-000000000000' !== $aaguid) {
$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
'aaguid' => $aaguid,
'AttestationStatement' => $attestationStatement,
]);
$attestedCredentialData->setAaguid(
Uuid::fromString('00000000-0000-0000-0000-000000000000')
);
return;
}
return;
}
//The MDS Repository is mandatory here
Assertion::notNull($this->metadataStatementRepository, 'The Metadata Statement Repository is mandatory when requesting attestation objects.');
$metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
// We check the last status report
$this->checkStatusReport(null === $metadataStatement ? [] : $metadataStatement->getStatusReports());
// We check the certificate chain (if any)
$this->checkCertificateChain($attestationStatement, $metadataStatement);
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
// => nothing to check
if ('00000000-0000-0000-0000-000000000000' === $aaguid || AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
return;
}
// At this point, the Metadata Statement is mandatory
Assertion::notNull($metadataStatement, sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid));
// Check Attestation Type is allowed
if (0 !== count($metadataStatement->getAttestationTypes())) {
$type = $this->getAttestationType($attestationStatement);
Assertion::inArray($type, $metadataStatement->getAttestationTypes(), 'Invalid attestation statement. The attestation type is not allowed for this authenticator');
}
}
/**
* @param StatusReport[] $statusReports
*/
private function checkStatusReport(array $statusReports): void
{
if (0 !== count($statusReports)) {
$lastStatusReport = end($statusReports);
if ($lastStatusReport->isCompromised()) {
throw new LogicException('The authenticator is compromised and cannot be used');
}
}
}
private function createPublicKeyCredentialSource(string $credentialId, AttestedCredentialData $attestedCredentialData, AttestationObject $attestationObject, string $userHandle): PublicKeyCredentialSource
{
return new PublicKeyCredentialSource(
$credentialId,
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
[],
$attestationObject->getAttStmt()->getType(),
$attestationObject->getAttStmt()->getTrustPath(),
$attestedCredentialData->getAaguid(),
$attestedCredentialData->getCredentialPublicKey(),
$userHandle,
$attestationObject->getAuthData()->getSignCount()
);
}
private function getAttestationType(AttestationStatement $attestationStatement): int
{
switch ($attestationStatement->getType()) {
case AttestationStatement::TYPE_BASIC:
return MetadataStatement::ATTESTATION_BASIC_FULL;
case AttestationStatement::TYPE_SELF:
return MetadataStatement::ATTESTATION_BASIC_SURROGATE;
case AttestationStatement::TYPE_ATTCA:
return MetadataStatement::ATTESTATION_ATTCA;
case AttestationStatement::TYPE_ECDAA:
return MetadataStatement::ATTESTATION_ECDAA;
default:
throw new InvalidArgumentException('Invalid attestation type');
}
}
private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
{
if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')->value();
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
if (!is_string($appId) || true !== $wasUsed) {
return $rpId;
}
return $appId;
}
}

View file

@ -1,124 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function ord;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
/**
* @see https://www.w3.org/TR/webauthn/#sec-authenticator-data
*/
class AuthenticatorData
{
private const FLAG_UP = 0b00000001;
private const FLAG_RFU1 = 0b00000010;
private const FLAG_UV = 0b00000100;
private const FLAG_RFU2 = 0b00111000;
private const FLAG_AT = 0b01000000;
private const FLAG_ED = 0b10000000;
/**
* @var string
*/
protected $authData;
/**
* @var string
*/
protected $rpIdHash;
/**
* @var string
*/
protected $flags;
/**
* @var int
*/
protected $signCount;
/**
* @var AttestedCredentialData|null
*/
protected $attestedCredentialData;
/**
* @var AuthenticationExtensionsClientOutputs|null
*/
protected $extensions;
public function __construct(string $authData, string $rpIdHash, string $flags, int $signCount, ?AttestedCredentialData $attestedCredentialData, ?AuthenticationExtensionsClientOutputs $extensions)
{
$this->rpIdHash = $rpIdHash;
$this->flags = $flags;
$this->signCount = $signCount;
$this->attestedCredentialData = $attestedCredentialData;
$this->extensions = $extensions;
$this->authData = $authData;
}
public function getAuthData(): string
{
return $this->authData;
}
public function getRpIdHash(): string
{
return $this->rpIdHash;
}
public function isUserPresent(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UP) ? true : false;
}
public function isUserVerified(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UV) ? true : false;
}
public function hasAttestedCredentialData(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_AT) ? true : false;
}
public function hasExtensions(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_ED) ? true : false;
}
public function getReservedForFutureUse1(): int
{
return ord($this->flags) & self::FLAG_RFU1;
}
public function getReservedForFutureUse2(): int
{
return ord($this->flags) & self::FLAG_RFU2;
}
public function getSignCount(): int
{
return $this->signCount;
}
public function getAttestedCredentialData(): ?AttestedCredentialData
{
return $this->attestedCredentialData;
}
public function getExtensions(): ?AuthenticationExtensionsClientOutputs
{
return null !== $this->extensions && $this->hasExtensions() ? $this->extensions : null;
}
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorresponse
*/
abstract class AuthenticatorResponse
{
/**
* @var CollectedClientData
*/
private $clientDataJSON;
public function __construct(CollectedClientData $clientDataJSON)
{
$this->clientDataJSON = $clientDataJSON;
}
public function getClientDataJSON(): CollectedClientData
{
return $this->clientDataJSON;
}
}

View file

@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use JsonSerializable;
use function Safe\json_decode;
class AuthenticatorSelectionCriteria implements JsonSerializable
{
public const AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE = null;
public const AUTHENTICATOR_ATTACHMENT_PLATFORM = 'platform';
public const AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 'cross-platform';
public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
public const RESIDENT_KEY_REQUIREMENT_NONE = null;
public const RESIDENT_KEY_REQUIREMENT_REQUIRED = 'required';
public const RESIDENT_KEY_REQUIREMENT_PREFERRED = 'preferred';
public const RESIDENT_KEY_REQUIREMENT_DISCOURAGED = 'discouraged';
/**
* @var string|null
*/
private $authenticatorAttachment;
/**
* @var bool
*/
private $requireResidentKey;
/**
* @var string
*/
private $userVerification;
/**
* @var string|null
*/
private $residentKey;
public function __construct(?string $authenticatorAttachment = null, ?bool $requireResidentKey = null, ?string $userVerification = null, ?string $residentKey = null)
{
if (null !== $authenticatorAttachment) {
@trigger_error('The argument "authenticatorAttachment" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setAuthenticatorAttachment".', E_USER_DEPRECATED);
}
if (null !== $requireResidentKey) {
@trigger_error('The argument "requireResidentKey" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setRequireResidentKey".', E_USER_DEPRECATED);
}
if (null !== $userVerification) {
@trigger_error('The argument "userVerification" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setUserVerification".', E_USER_DEPRECATED);
}
if (null !== $residentKey) {
@trigger_error('The argument "residentKey" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setResidentKey".', E_USER_DEPRECATED);
}
$this->authenticatorAttachment = $authenticatorAttachment;
$this->requireResidentKey = $requireResidentKey ?? false;
$this->userVerification = $userVerification ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
$this->residentKey = $residentKey ?? self::RESIDENT_KEY_REQUIREMENT_NONE;
}
public static function create(): self
{
return new self();
}
public function setAuthenticatorAttachment(?string $authenticatorAttachment): self
{
$this->authenticatorAttachment = $authenticatorAttachment;
return $this;
}
public function setRequireResidentKey(bool $requireResidentKey): self
{
$this->requireResidentKey = $requireResidentKey;
return $this;
}
public function setUserVerification(string $userVerification): self
{
$this->userVerification = $userVerification;
return $this;
}
public function setResidentKey(?string $residentKey): self
{
$this->residentKey = $residentKey;
return $this;
}
public function getAuthenticatorAttachment(): ?string
{
return $this->authenticatorAttachment;
}
public function isRequireResidentKey(): bool
{
return $this->requireResidentKey;
}
public function getUserVerification(): string
{
return $this->userVerification;
}
public function getResidentKey(): ?string
{
return $this->residentKey;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
return self::create()
->setAuthenticatorAttachment($json['authenticatorAttachment'] ?? null)
->setRequireResidentKey($json['requireResidentKey'] ?? false)
->setUserVerification($json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED)
->setResidentKey($json['residentKey'] ?? self::RESIDENT_KEY_REQUIREMENT_NONE)
;
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'requireResidentKey' => $this->requireResidentKey,
'userVerification' => $this->userVerification,
];
if (null !== $this->authenticatorAttachment) {
$json['authenticatorAttachment'] = $this->authenticatorAttachment;
}
if (null !== $this->residentKey) {
$json['residentKey'] = $this->residentKey;
}
return $json;
}
}

View file

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\CertificateChainChecker;
interface CertificateChainChecker
{
/**
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public function check(array $authenticatorCertificates, array $trustedCertificates): void;
}

View file

@ -1,239 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\CertificateChainChecker;
use Assert\Assertion;
use function count;
use InvalidArgumentException;
use function is_int;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use Safe\Exceptions\FilesystemException;
use function Safe\file_put_contents;
use function Safe\ksort;
use function Safe\mkdir;
use function Safe\rename;
use function Safe\sprintf;
use function Safe\tempnam;
use function Safe\unlink;
use Symfony\Component\Process\Process;
final class OpenSSLCertificateChainChecker implements CertificateChainChecker
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var string[]
*/
private $rootCertificates = [];
public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory)
{
$this->client = $client;
$this->requestFactory = $requestFactory;
}
public function addRootCertificate(string $certificate): self
{
$this->rootCertificates[] = $certificate;
return $this;
}
/**
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public function check(array $authenticatorCertificates, array $trustedCertificates): void
{
if (0 === count($trustedCertificates)) {
$this->checkCertificatesValidity($authenticatorCertificates, true);
return;
}
$this->checkCertificatesValidity($authenticatorCertificates, false);
$hasCrls = false;
$processArguments = ['-no-CAfile', '-no-CApath'];
$caDirname = $this->createTemporaryDirectory();
$processArguments[] = '--CApath';
$processArguments[] = $caDirname;
foreach ($trustedCertificates as $certificate) {
$this->saveToTemporaryFile($caDirname, $certificate, 'webauthn-trusted-', '.pem');
$crl = $this->getCrls($certificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-trusted-crl-', '.crl');
}
}
$rehashProcess = new Process(['openssl', 'rehash', $caDirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
if (!$rehashProcess->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
$filenames = [];
$leafCertificate = array_shift($authenticatorCertificates);
$leafFilename = $this->saveToTemporaryFile(sys_get_temp_dir(), $leafCertificate, 'webauthn-leaf-', '.pem');
$crl = $this->getCrls($leafCertificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-leaf-crl-', '.pem');
}
$filenames[] = $leafFilename;
foreach ($authenticatorCertificates as $certificate) {
$untrustedFilename = $this->saveToTemporaryFile(sys_get_temp_dir(), $certificate, 'webauthn-untrusted-', '.pem');
$crl = $this->getCrls($certificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-untrusted-crl-', '.pem');
}
$processArguments[] = '-untrusted';
$processArguments[] = $untrustedFilename;
$filenames[] = $untrustedFilename;
}
$processArguments[] = $leafFilename;
if ($hasCrls) {
array_unshift($processArguments, '-crl_check');
array_unshift($processArguments, '-crl_check_all');
//array_unshift($processArguments, '-crl_download');
array_unshift($processArguments, '-extended_crl');
}
array_unshift($processArguments, 'openssl', 'verify');
$process = new Process($processArguments);
$process->run();
while ($process->isRunning()) {
//Just wait
}
foreach ($filenames as $filename) {
try {
unlink($filename);
} catch (FilesystemException $e) {
continue;
}
}
$this->deleteDirectory($caDirname);
if (!$process->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
}
/**
* @param string[] $certificates
*/
private function checkCertificatesValidity(array $certificates, bool $allowRootCertificate): void
{
foreach ($certificates as $certificate) {
$parsed = openssl_x509_parse($certificate);
Assertion::isArray($parsed, 'Unable to read the certificate');
if (false === $allowRootCertificate) {
$this->checkRootCertificate($parsed);
}
Assertion::keyExists($parsed, 'validTo_time_t', 'The certificate has no validity period');
Assertion::keyExists($parsed, 'validFrom_time_t', 'The certificate has no validity period');
Assertion::lessOrEqualThan(time(), $parsed['validTo_time_t'], 'The certificate expired');
Assertion::greaterOrEqualThan(time(), $parsed['validFrom_time_t'], 'The certificate is not usable yet');
}
}
/**
* @param array<string, mixed> $parsed
*/
private function checkRootCertificate(array $parsed): void
{
Assertion::keyExists($parsed, 'subject', 'The certificate has no subject');
Assertion::keyExists($parsed, 'issuer', 'The certificate has no issuer');
$subject = $parsed['subject'];
$issuer = $parsed['issuer'];
ksort($subject);
ksort($issuer);
Assertion::notEq($subject, $issuer, 'Root certificates are not allowed');
}
private function createTemporaryDirectory(): string
{
$caDir = tempnam(sys_get_temp_dir(), 'webauthn-ca-');
if (file_exists($caDir)) {
unlink($caDir);
}
mkdir($caDir);
if (!is_dir($caDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $caDir));
}
return $caDir;
}
private function deleteDirectory(string $dirname): void
{
$rehashProcess = new Process(['rm', '-rf', $dirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
}
private function saveToTemporaryFile(string $folder, string $certificate, string $prefix, string $suffix): string
{
$filename = tempnam($folder, $prefix);
rename($filename, $filename.$suffix);
file_put_contents($filename.$suffix, $certificate, FILE_APPEND);
return $filename.$suffix;
}
private function getCrls(string $certificate): string
{
$parsed = openssl_x509_parse($certificate);
if (false === $parsed || !isset($parsed['extensions']['crlDistributionPoints'])) {
return '';
}
$endpoint = $parsed['extensions']['crlDistributionPoints'];
$pos = mb_strpos($endpoint, 'URI:');
if (!is_int($pos)) {
return '';
}
$endpoint = trim(mb_substr($endpoint, $pos + 4));
$request = $this->requestFactory->createRequest('GET', $endpoint);
$response = $this->client->sendRequest($request);
if (200 !== $response->getStatusCode()) {
return '';
}
return $response->getBody()->getContents();
}
}

View file

@ -1,223 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function count;
use function in_array;
use InvalidArgumentException;
use RuntimeException;
use Safe\Exceptions\FilesystemException;
use function Safe\file_put_contents;
use function Safe\ksort;
use function Safe\mkdir;
use function Safe\rename;
use function Safe\sprintf;
use function Safe\tempnam;
use function Safe\unlink;
use Symfony\Component\Process\Process;
class CertificateToolbox
{
/**
* @deprecated "This method is deprecated since v3.3 and will be removed en v4.0. Please use Webauthn\CertificateChainChecker\CertificateChainChecker instead"
*
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public static function checkChain(array $authenticatorCertificates, array $trustedCertificates = []): void
{
if (0 === count($trustedCertificates)) {
self::checkCertificatesValidity($authenticatorCertificates, true);
return;
}
self::checkCertificatesValidity($authenticatorCertificates, false);
$processArguments = ['-no-CAfile', '-no-CApath'];
$caDirname = self::createTemporaryDirectory();
$processArguments[] = '--CApath';
$processArguments[] = $caDirname;
foreach ($trustedCertificates as $certificate) {
self::prepareCertificate($caDirname, $certificate, 'webauthn-trusted-', '.pem');
}
$rehashProcess = new Process(['openssl', 'rehash', $caDirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
if (!$rehashProcess->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
$filenames = [];
$leafCertificate = array_shift($authenticatorCertificates);
$leafFilename = self::prepareCertificate(sys_get_temp_dir(), $leafCertificate, 'webauthn-leaf-', '.pem');
$filenames[] = $leafFilename;
foreach ($authenticatorCertificates as $certificate) {
$untrustedFilename = self::prepareCertificate(sys_get_temp_dir(), $certificate, 'webauthn-untrusted-', '.pem');
$processArguments[] = '-untrusted';
$processArguments[] = $untrustedFilename;
$filenames[] = $untrustedFilename;
}
$processArguments[] = $leafFilename;
array_unshift($processArguments, 'openssl', 'verify');
$process = new Process($processArguments);
$process->run();
while ($process->isRunning()) {
//Just wait
}
foreach ($filenames as $filename) {
try {
unlink($filename);
} catch (FilesystemException $e) {
continue;
}
}
self::deleteDirectory($caDirname);
if (!$process->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
}
public static function fixPEMStructure(string $certificate, string $type = 'CERTIFICATE'): string
{
$pemCert = '-----BEGIN '.$type.'-----'.PHP_EOL;
$pemCert .= chunk_split($certificate, 64, PHP_EOL);
$pemCert .= '-----END '.$type.'-----'.PHP_EOL;
return $pemCert;
}
public static function convertDERToPEM(string $certificate, string $type = 'CERTIFICATE'): string
{
$derCertificate = self::unusedBytesFix($certificate);
return self::fixPEMStructure(base64_encode($derCertificate), $type);
}
/**
* @param string[] $certificates
*
* @return string[]
*/
public static function convertAllDERToPEM(array $certificates, string $type = 'CERTIFICATE'): array
{
$certs = [];
foreach ($certificates as $publicKey) {
$certs[] = self::convertDERToPEM($publicKey, $type);
}
return $certs;
}
private static function unusedBytesFix(string $certificate): string
{
$certificateHash = hash('sha256', $certificate);
if (in_array($certificateHash, self::getCertificateHashes(), true)) {
$certificate[mb_strlen($certificate, '8bit') - 257] = "\0";
}
return $certificate;
}
/**
* @param string[] $certificates
*/
private static function checkCertificatesValidity(array $certificates, bool $allowRootCertificate): void
{
foreach ($certificates as $certificate) {
$parsed = openssl_x509_parse($certificate);
Assertion::isArray($parsed, 'Unable to read the certificate');
if (false === $allowRootCertificate) {
self::checkRootCertificate($parsed);
}
Assertion::keyExists($parsed, 'validTo_time_t', 'The certificate has no validity period');
Assertion::keyExists($parsed, 'validFrom_time_t', 'The certificate has no validity period');
Assertion::lessOrEqualThan(time(), $parsed['validTo_time_t'], 'The certificate expired');
Assertion::greaterOrEqualThan(time(), $parsed['validFrom_time_t'], 'The certificate is not usable yet');
}
}
/**
* @param array<string, mixed> $parsed
*/
private static function checkRootCertificate(array $parsed): void
{
Assertion::keyExists($parsed, 'subject', 'The certificate has no subject');
Assertion::keyExists($parsed, 'issuer', 'The certificate has no issuer');
$subject = $parsed['subject'];
$issuer = $parsed['issuer'];
ksort($subject);
ksort($issuer);
Assertion::notEq($subject, $issuer, 'Root certificates are not allowed');
}
/**
* @return string[]
*/
private static function getCertificateHashes(): array
{
return [
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
];
}
private static function createTemporaryDirectory(): string
{
$caDir = tempnam(sys_get_temp_dir(), 'webauthn-ca-');
if (file_exists($caDir)) {
unlink($caDir);
}
mkdir($caDir);
if (!is_dir($caDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $caDir));
}
return $caDir;
}
private static function deleteDirectory(string $dirname): void
{
$rehashProcess = new Process(['rm', '-rf', $dirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
}
private static function prepareCertificate(string $folder, string $certificate, string $prefix, string $suffix): string
{
$untrustedFilename = tempnam($folder, $prefix);
rename($untrustedFilename, $untrustedFilename.$suffix);
file_put_contents($untrustedFilename.$suffix, $certificate, FILE_APPEND);
file_put_contents($untrustedFilename.$suffix, PHP_EOL, FILE_APPEND);
return $untrustedFilename.$suffix;
}
}

View file

@ -1,145 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function array_key_exists;
use Assert\Assertion;
use Base64Url\Base64Url;
use InvalidArgumentException;
use function Safe\json_decode;
use function Safe\sprintf;
use Webauthn\TokenBinding\TokenBinding;
class CollectedClientData
{
/**
* @var string
*/
private $rawData;
/**
* @var mixed[]
*/
private $data;
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $challenge;
/**
* @var string
*/
private $origin;
/**
* @var mixed[]|null
*/
private $tokenBinding;
/**
* @param mixed[] $data
*/
public function __construct(string $rawData, array $data)
{
$this->type = $this->findData($data, 'type');
$this->challenge = $this->findData($data, 'challenge', true, true);
$this->origin = $this->findData($data, 'origin');
$this->tokenBinding = $this->findData($data, 'tokenBinding', false);
$this->rawData = $rawData;
$this->data = $data;
}
public static function createFormJson(string $data): self
{
$rawData = Base64Url::decode($data);
$json = json_decode($rawData, true);
Assertion::isArray($json, 'Invalid collected client data');
return new self($rawData, $json);
}
public function getType(): string
{
return $this->type;
}
public function getChallenge(): string
{
return $this->challenge;
}
public function getOrigin(): string
{
return $this->origin;
}
public function getTokenBinding(): ?TokenBinding
{
return null === $this->tokenBinding ? null : TokenBinding::createFormArray($this->tokenBinding);
}
public function getRawData(): string
{
return $this->rawData;
}
/**
* @return string[]
*/
public function all(): array
{
return array_keys($this->data);
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
/**
* @return mixed
*/
public function get(string $key)
{
if (!$this->has($key)) {
throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key));
}
return $this->data[$key];
}
/**
* @param mixed[] $json
*
* @return mixed|null
*/
private function findData(array $json, string $key, bool $isRequired = true, bool $isB64 = false)
{
if (!array_key_exists($key, $json)) {
if ($isRequired) {
throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key));
}
return;
}
return $isB64 ? Base64Url::decode($json[$key]) : $json[$key];
}
}

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\Counter;
use Webauthn\PublicKeyCredentialSource;
interface CounterChecker
{
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void;
}

View file

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\Counter;
use Assert\Assertion;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;
use Webauthn\PublicKeyCredentialSource;
final class ThrowExceptionIfInvalid implements CounterChecker
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger ?? new NullLogger();
}
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void
{
try {
Assertion::greaterThan($currentCounter, $publicKeyCredentialSource->getCounter(), 'Invalid counter.');
} catch (Throwable $throwable) {
$this->logger->error('The counter is invalid', [
'current' => $currentCounter,
'new' => $publicKeyCredentialSource->getCounter(),
]);
throw $throwable;
}
}
}

View file

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
/**
* @see https://w3c.github.io/webappsec-credential-management/#credential
*/
abstract class Credential
{
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $type;
public function __construct(string $id, string $type)
{
$this->id = $id;
$this->type = $type;
}
public function getId(): string
{
return $this->id;
}
public function getType(): string
{
return $this->type;
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function Safe\json_encode;
/**
* @see https://www.w3.org/TR/webauthn/#iface-pkcredential
*/
class PublicKeyCredential extends Credential
{
/**
* @var string
*/
protected $rawId;
/**
* @var AuthenticatorResponse
*/
protected $response;
public function __construct(string $id, string $type, string $rawId, AuthenticatorResponse $response)
{
parent::__construct($id, $type);
$this->rawId = $rawId;
$this->response = $response;
}
public function __toString()
{
return json_encode($this);
}
public function getRawId(): string
{
return $this->rawId;
}
public function getResponse(): AuthenticatorResponse
{
return $this->response;
}
/**
* @param string[] $transport
*/
public function getPublicKeyCredentialDescriptor(array $transport = []): PublicKeyCredentialDescriptor
{
return new PublicKeyCredentialDescriptor($this->getType(), $this->getRawId(), $transport);
}
}

View file

@ -1,261 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use Base64Url\Base64Url;
use function count;
use function Safe\json_decode;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
class PublicKeyCredentialCreationOptions extends PublicKeyCredentialOptions
{
public const ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 'none';
public const ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 'indirect';
public const ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 'direct';
public const ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE = 'enterprise';
/**
* @var PublicKeyCredentialRpEntity
*/
private $rp;
/**
* @var PublicKeyCredentialUserEntity
*/
private $user;
/**
* @var PublicKeyCredentialParameters[]
*/
private $pubKeyCredParams = [];
/**
* @var PublicKeyCredentialDescriptor[]
*/
private $excludeCredentials = [];
/**
* @var AuthenticatorSelectionCriteria
*/
private $authenticatorSelection;
/**
* @var string
*/
private $attestation;
/**
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
* @param PublicKeyCredentialDescriptor[] $excludeCredentials
*/
public function __construct(PublicKeyCredentialRpEntity $rp, PublicKeyCredentialUserEntity $user, string $challenge, array $pubKeyCredParams, ?int $timeout = null, array $excludeCredentials = [], ?AuthenticatorSelectionCriteria $authenticatorSelection = null, ?string $attestation = null, ?AuthenticationExtensionsClientInputs $extensions = null)
{
if (0 !== count($excludeCredentials)) {
@trigger_error('The argument "excludeCredentials" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "excludeCredentials" or "excludeCredential".', E_USER_DEPRECATED);
}
if (null !== $authenticatorSelection) {
@trigger_error('The argument "authenticatorSelection" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setAuthenticatorSelection".', E_USER_DEPRECATED);
}
if (null !== $attestation) {
@trigger_error('The argument "attestation" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setAttestation".', E_USER_DEPRECATED);
}
parent::__construct($challenge, $timeout, $extensions);
$this->rp = $rp;
$this->user = $user;
$this->pubKeyCredParams = $pubKeyCredParams;
$this->authenticatorSelection = $authenticatorSelection ?? new AuthenticatorSelectionCriteria();
$this->attestation = $attestation ?? self::ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
$this->excludeCredentials($excludeCredentials)
;
}
/**
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
*/
public static function create(PublicKeyCredentialRpEntity $rp, PublicKeyCredentialUserEntity $user, string $challenge, array $pubKeyCredParams): self
{
return new self($rp, $user, $challenge, $pubKeyCredParams);
}
public function addPubKeyCredParam(PublicKeyCredentialParameters $pubKeyCredParam): self
{
$this->pubKeyCredParams[] = $pubKeyCredParam;
return $this;
}
/**
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
*/
public function addPubKeyCredParams(array $pubKeyCredParams): self
{
foreach ($pubKeyCredParams as $pubKeyCredParam) {
$this->addPubKeyCredParam($pubKeyCredParam);
}
return $this;
}
public function excludeCredential(PublicKeyCredentialDescriptor $excludeCredential): self
{
$this->excludeCredentials[] = $excludeCredential;
return $this;
}
/**
* @param PublicKeyCredentialDescriptor[] $excludeCredentials
*/
public function excludeCredentials(array $excludeCredentials): self
{
foreach ($excludeCredentials as $excludeCredential) {
$this->excludeCredential($excludeCredential);
}
return $this;
}
public function setAuthenticatorSelection(AuthenticatorSelectionCriteria $authenticatorSelection): self
{
$this->authenticatorSelection = $authenticatorSelection;
return $this;
}
public function setAttestation(string $attestation): self
{
Assertion::inArray($attestation, [
self::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
self::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT,
self::ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT,
self::ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE,
], 'Invalid attestation conveyance mode');
$this->attestation = $attestation;
return $this;
}
public function getRp(): PublicKeyCredentialRpEntity
{
return $this->rp;
}
public function getUser(): PublicKeyCredentialUserEntity
{
return $this->user;
}
/**
* @return PublicKeyCredentialParameters[]
*/
public function getPubKeyCredParams(): array
{
return $this->pubKeyCredParams;
}
/**
* @return PublicKeyCredentialDescriptor[]
*/
public function getExcludeCredentials(): array
{
return $this->excludeCredentials;
}
public function getAuthenticatorSelection(): AuthenticatorSelectionCriteria
{
return $this->authenticatorSelection;
}
public function getAttestation(): string
{
return $this->attestation;
}
public static function createFromString(string $data): PublicKeyCredentialOptions
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
public static function createFromArray(array $json): PublicKeyCredentialOptions
{
Assertion::keyExists($json, 'rp', 'Invalid input. "rp" is missing.');
Assertion::keyExists($json, 'pubKeyCredParams', 'Invalid input. "pubKeyCredParams" is missing.');
Assertion::isArray($json['pubKeyCredParams'], 'Invalid input. "pubKeyCredParams" is not an array.');
Assertion::keyExists($json, 'challenge', 'Invalid input. "challenge" is missing.');
Assertion::keyExists($json, 'attestation', 'Invalid input. "attestation" is missing.');
Assertion::keyExists($json, 'user', 'Invalid input. "user" is missing.');
Assertion::keyExists($json, 'authenticatorSelection', 'Invalid input. "authenticatorSelection" is missing.');
$pubKeyCredParams = [];
foreach ($json['pubKeyCredParams'] as $pubKeyCredParam) {
$pubKeyCredParams[] = PublicKeyCredentialParameters::createFromArray($pubKeyCredParam);
}
$excludeCredentials = [];
if (isset($json['excludeCredentials'])) {
foreach ($json['excludeCredentials'] as $excludeCredential) {
$excludeCredentials[] = PublicKeyCredentialDescriptor::createFromArray($excludeCredential);
}
}
return self::create(
PublicKeyCredentialRpEntity::createFromArray($json['rp']),
PublicKeyCredentialUserEntity::createFromArray($json['user']),
Base64Url::decode($json['challenge']),
$pubKeyCredParams
)
->excludeCredentials($excludeCredentials)
->setAuthenticatorSelection(AuthenticatorSelectionCriteria::createFromArray($json['authenticatorSelection']))
->setAttestation($json['attestation'])
->setTimeout($json['timeout'] ?? null)
->setExtensions(isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray($json['extensions']) : new AuthenticationExtensionsClientInputs())
;
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'rp' => $this->rp->jsonSerialize(),
'pubKeyCredParams' => array_map(static function (PublicKeyCredentialParameters $object): array {
return $object->jsonSerialize();
}, $this->pubKeyCredParams),
'challenge' => Base64Url::encode($this->challenge),
'attestation' => $this->attestation,
'user' => $this->user->jsonSerialize(),
'authenticatorSelection' => $this->authenticatorSelection->jsonSerialize(),
];
if (0 !== count($this->excludeCredentials)) {
$json['excludeCredentials'] = array_map(static function (PublicKeyCredentialDescriptor $object): array {
return $object->jsonSerialize();
}, $this->excludeCredentials);
}
if (0 !== $this->extensions->count()) {
$json['extensions'] = $this->extensions;
}
if (null !== $this->timeout) {
$json['timeout'] = $this->timeout;
}
return $json;
}
}

View file

@ -1,112 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use Base64Url\Base64Url;
use function count;
use JsonSerializable;
use function Safe\json_decode;
class PublicKeyCredentialDescriptor implements JsonSerializable
{
public const CREDENTIAL_TYPE_PUBLIC_KEY = 'public-key';
public const AUTHENTICATOR_TRANSPORT_USB = 'usb';
public const AUTHENTICATOR_TRANSPORT_NFC = 'nfc';
public const AUTHENTICATOR_TRANSPORT_BLE = 'ble';
public const AUTHENTICATOR_TRANSPORT_INTERNAL = 'internal';
/**
* @var string
*/
protected $type;
/**
* @var string
*/
protected $id;
/**
* @var string[]
*/
protected $transports;
/**
* @param string[] $transports
*/
public function __construct(string $type, string $id, array $transports = [])
{
$this->type = $type;
$this->id = $id;
$this->transports = $transports;
}
public function getType(): string
{
return $this->type;
}
public function getId(): string
{
return $this->id;
}
/**
* @return string[]
*/
public function getTransports(): array
{
return $this->transports;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'type', 'Invalid input. "type" is missing.');
Assertion::keyExists($json, 'id', 'Invalid input. "id" is missing.');
return new self(
$json['type'],
Base64Url::decode($json['id']),
$json['transports'] ?? []
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'type' => $this->type,
'id' => Base64Url::encode($this->id),
];
if (0 !== count($this->transports)) {
$json['transports'] = $this->transports;
}
return $json;
}
}

View file

@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function array_key_exists;
use ArrayIterator;
use Assert\Assertion;
use function count;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use function Safe\json_decode;
class PublicKeyCredentialDescriptorCollection implements JsonSerializable, Countable, IteratorAggregate
{
/**
* @var PublicKeyCredentialDescriptor[]
*/
private $publicKeyCredentialDescriptors = [];
public function add(PublicKeyCredentialDescriptor $publicKeyCredentialDescriptor): void
{
$this->publicKeyCredentialDescriptors[$publicKeyCredentialDescriptor->getId()] = $publicKeyCredentialDescriptor;
}
public function has(string $id): bool
{
return array_key_exists($id, $this->publicKeyCredentialDescriptors);
}
public function remove(string $id): void
{
if (!$this->has($id)) {
return;
}
unset($this->publicKeyCredentialDescriptors[$id]);
}
/**
* @return Iterator<string, PublicKeyCredentialDescriptor>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->publicKeyCredentialDescriptors);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->publicKeyCredentialDescriptors, $mode);
}
/**
* @return array[]
*/
public function jsonSerialize(): array
{
return array_map(static function (PublicKeyCredentialDescriptor $object): array {
return $object->jsonSerialize();
}, $this->publicKeyCredentialDescriptors);
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
$collection = new self();
foreach ($json as $item) {
$collection->add(PublicKeyCredentialDescriptor::createFromArray($item));
}
return $collection;
}
}

View file

@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use JsonSerializable;
abstract class PublicKeyCredentialEntity implements JsonSerializable
{
/**
* @var string
*/
protected $name;
/**
* @var string|null
*/
protected $icon;
public function __construct(string $name, ?string $icon)
{
$this->name = $name;
$this->icon = $icon;
}
public function getName(): string
{
return $this->name;
}
public function getIcon(): ?string
{
return $this->icon;
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'name' => $this->name,
];
if (null !== $this->icon) {
$json['icon'] = $this->icon;
}
return $json;
}
}

View file

@ -1,181 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function array_key_exists;
use Assert\Assertion;
use Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use InvalidArgumentException;
use function ord;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\json_decode;
use function Safe\sprintf;
use function Safe\unpack;
use Throwable;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
class PublicKeyCredentialLoader
{
private const FLAG_AT = 0b01000000;
private const FLAG_ED = 0b10000000;
/**
* @var AttestationObjectLoader
*/
private $attestationObjectLoader;
/**
* @var Decoder
*/
private $decoder;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(AttestationObjectLoader $attestationObjectLoader, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->attestationObjectLoader = $attestationObjectLoader;
$this->logger = $logger ?? new NullLogger();
}
public static function create(AttestationObjectLoader $attestationObjectLoader): self
{
return new self($attestationObjectLoader);
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
/**
* @param mixed[] $json
*/
public function loadArray(array $json): PublicKeyCredential
{
$this->logger->info('Trying to load data from an array', ['data' => $json]);
try {
foreach (['id', 'rawId', 'type'] as $key) {
Assertion::keyExists($json, $key, sprintf('The parameter "%s" is missing', $key));
Assertion::string($json[$key], sprintf('The parameter "%s" shall be a string', $key));
}
Assertion::keyExists($json, 'response', 'The parameter "response" is missing');
Assertion::isArray($json['response'], 'The parameter "response" shall be an array');
Assertion::eq($json['type'], 'public-key', sprintf('Unsupported type "%s"', $json['type']));
$id = Base64Url::decode($json['id']);
$rawId = Base64Url::decode($json['rawId']);
Assertion::true(hash_equals($id, $rawId));
$publicKeyCredential = new PublicKeyCredential(
$json['id'],
$json['type'],
$rawId,
$this->createResponse($json['response'])
);
$this->logger->info('The data has been loaded');
$this->logger->debug('Public Key Credential', ['publicKeyCredential' => $publicKeyCredential]);
return $publicKeyCredential;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function load(string $data): PublicKeyCredential
{
$this->logger->info('Trying to load data from a string', ['data' => $data]);
try {
$json = json_decode($data, true);
return $this->loadArray($json);
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
/**
* @param mixed[] $response
*/
private function createResponse(array $response): AuthenticatorResponse
{
Assertion::keyExists($response, 'clientDataJSON', 'Invalid data. The parameter "clientDataJSON" is missing');
Assertion::string($response['clientDataJSON'], 'Invalid data. The parameter "clientDataJSON" is invalid');
switch (true) {
case array_key_exists('attestationObject', $response):
Assertion::string($response['attestationObject'], 'Invalid data. The parameter "attestationObject " is invalid');
$attestationObject = $this->attestationObjectLoader->load($response['attestationObject']);
return new AuthenticatorAttestationResponse(CollectedClientData::createFormJson($response['clientDataJSON']), $attestationObject);
case array_key_exists('authenticatorData', $response) && array_key_exists('signature', $response):
$authData = Base64Url::decode($response['authenticatorData']);
$authDataStream = new StringStream($authData);
$rp_id_hash = $authDataStream->read(32);
$flags = $authDataStream->read(1);
$signCount = $authDataStream->read(4);
$signCount = unpack('N', $signCount)[1];
$attestedCredentialData = null;
if (0 !== (ord($flags) & self::FLAG_AT)) {
$aaguid = Uuid::fromBytes($authDataStream->read(16));
$credentialLength = $authDataStream->read(2);
$credentialLength = unpack('n', $credentialLength)[1];
$credentialId = $authDataStream->read($credentialLength);
$credentialPublicKey = $this->decoder->decode($authDataStream);
Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
$attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
}
$extension = null;
if (0 !== (ord($flags) & self::FLAG_ED)) {
$extension = $this->decoder->decode($authDataStream);
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
}
Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
$authDataStream->close();
$authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
return new AuthenticatorAssertionResponse(
CollectedClientData::createFormJson($response['clientDataJSON']),
$authenticatorData,
Base64Url::decode($response['signature']),
$response['userHandle'] ?? null
);
default:
throw new InvalidArgumentException('Unable to create the response object');
}
}
}

View file

@ -1,104 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use JsonSerializable;
use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
abstract class PublicKeyCredentialOptions implements JsonSerializable
{
/**
* @var string
*/
protected $challenge;
/**
* @var int|null
*/
protected $timeout;
/**
* @var AuthenticationExtensionsClientInputs
*/
protected $extensions;
public function __construct(string $challenge, ?int $timeout = null, ?AuthenticationExtensionsClientInputs $extensions = null)
{
if (null !== $timeout) {
@trigger_error('The argument "timeout" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setTimeout".', E_USER_DEPRECATED);
}
if (null !== $extensions) {
@trigger_error('The argument "extensions" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "addExtension" or "addExtensions".', E_USER_DEPRECATED);
}
$this->challenge = $challenge;
$this->setTimeout($timeout);
$this->extensions = $extensions ?? new AuthenticationExtensionsClientInputs();
}
public function setTimeout(?int $timeout): self
{
$this->timeout = $timeout;
return $this;
}
public function addExtension(AuthenticationExtension $extension): self
{
$this->extensions->add($extension);
return $this;
}
/**
* @param AuthenticationExtension[] $extensions
*/
public function addExtensions(array $extensions): self
{
foreach ($extensions as $extension) {
$this->addExtension($extension);
}
return $this;
}
public function setExtensions(AuthenticationExtensionsClientInputs $extensions): self
{
$this->extensions = $extensions;
return $this;
}
public function getChallenge(): string
{
return $this->challenge;
}
public function getTimeout(): ?int
{
return $this->timeout;
}
public function getExtensions(): AuthenticationExtensionsClientInputs
{
return $this->extensions;
}
abstract public static function createFromString(string $data): self;
/**
* @param mixed[] $json
*/
abstract public static function createFromArray(array $json): self;
}

View file

@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use JsonSerializable;
use function Safe\json_decode;
class PublicKeyCredentialParameters implements JsonSerializable
{
/**
* @var string
*/
private $type;
/**
* @var int
*/
private $alg;
public function __construct(string $type, int $alg)
{
$this->type = $type;
$this->alg = $alg;
}
public function getType(): string
{
return $this->type;
}
public function getAlg(): int
{
return $this->alg;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'type', 'Invalid input. "type" is missing.');
Assertion::string($json['type'], 'Invalid input. "type" is not a string.');
Assertion::keyExists($json, 'alg', 'Invalid input. "alg" is missing.');
Assertion::integer($json['alg'], 'Invalid input. "alg" is not an integer.');
return new self(
$json['type'],
$json['alg']
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
return [
'type' => $this->type,
'alg' => $this->alg,
];
}
}

View file

@ -1,194 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use Base64Url\Base64Url;
use function count;
use function Safe\json_decode;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
class PublicKeyCredentialRequestOptions extends PublicKeyCredentialOptions
{
public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
/**
* @var string|null
*/
private $rpId;
/**
* @var PublicKeyCredentialDescriptor[]
*/
private $allowCredentials = [];
/**
* @var string|null
*/
private $userVerification;
/**
* @param PublicKeyCredentialDescriptor[] $allowCredentials
*/
public function __construct(string $challenge, ?int $timeout = null, ?string $rpId = null, array $allowCredentials = [], ?string $userVerification = null, ?AuthenticationExtensionsClientInputs $extensions = null)
{
if (0 !== count($allowCredentials)) {
@trigger_error('The argument "allowCredentials" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "addAllowedCredentials" or "addAllowedCredential".', E_USER_DEPRECATED);
}
if (null !== $rpId) {
@trigger_error('The argument "rpId" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setRpId".', E_USER_DEPRECATED);
}
if (null !== $userVerification) {
@trigger_error('The argument "userVerification" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setUserVerification".', E_USER_DEPRECATED);
}
parent::__construct($challenge, $timeout, $extensions);
$this
->setRpId($rpId)
->allowCredentials($allowCredentials)
->setUserVerification($userVerification)
;
}
public static function create(string $challenge): self
{
return new self($challenge);
}
public function setRpId(?string $rpId): self
{
$this->rpId = $rpId;
return $this;
}
public function allowCredential(PublicKeyCredentialDescriptor $allowCredential): self
{
$this->allowCredentials[] = $allowCredential;
return $this;
}
/**
* @param PublicKeyCredentialDescriptor[] $allowCredentials
*/
public function allowCredentials(array $allowCredentials): self
{
foreach ($allowCredentials as $allowCredential) {
$this->allowCredential($allowCredential);
}
return $this;
}
public function setUserVerification(?string $userVerification): self
{
if (null === $userVerification) {
$this->rpId = null;
return $this;
}
Assertion::inArray($userVerification, [
self::USER_VERIFICATION_REQUIREMENT_REQUIRED,
self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
self::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
], 'Invalid user verification requirement');
$this->userVerification = $userVerification;
return $this;
}
public function getRpId(): ?string
{
return $this->rpId;
}
/**
* @return PublicKeyCredentialDescriptor[]
*/
public function getAllowCredentials(): array
{
return $this->allowCredentials;
}
public function getUserVerification(): ?string
{
return $this->userVerification;
}
public static function createFromString(string $data): PublicKeyCredentialOptions
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): PublicKeyCredentialOptions
{
Assertion::keyExists($json, 'challenge', 'Invalid input. "challenge" is missing.');
$allowCredentials = [];
$allowCredentialList = $json['allowCredentials'] ?? [];
foreach ($allowCredentialList as $allowCredential) {
$allowCredentials[] = PublicKeyCredentialDescriptor::createFromArray($allowCredential);
}
return self::create(Base64Url::decode($json['challenge']))
->setRpId($json['rpId'] ?? null)
->allowCredentials($allowCredentials)
->setUserVerification($json['userVerification'] ?? null)
->setTimeout($json['timeout'] ?? null)
->setExtensions(isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray($json['extensions']) : new AuthenticationExtensionsClientInputs())
;
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'challenge' => Base64Url::encode($this->challenge),
];
if (null !== $this->rpId) {
$json['rpId'] = $this->rpId;
}
if (null !== $this->userVerification) {
$json['userVerification'] = $this->userVerification;
}
if (0 !== count($this->allowCredentials)) {
$json['allowCredentials'] = array_map(static function (PublicKeyCredentialDescriptor $object): array {
return $object->jsonSerialize();
}, $this->allowCredentials);
}
if (0 !== $this->extensions->count()) {
$json['extensions'] = $this->extensions->jsonSerialize();
}
if (null !== $this->timeout) {
$json['timeout'] = $this->timeout;
}
return $json;
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity
{
/**
* @var string|null
*/
protected $id;
public function __construct(string $name, ?string $id = null, ?string $icon = null)
{
parent::__construct($name, $icon);
$this->id = $id;
}
public function getId(): ?string
{
return $this->id;
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'name', 'Invalid input. "name" is missing.');
return new self(
$json['name'],
$json['id'] ?? null,
$json['icon'] ?? null
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = parent::jsonSerialize();
if (null !== $this->id) {
$json['id'] = $this->id;
}
return $json;
}
}

View file

@ -1,240 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use Base64Url\Base64Url;
use InvalidArgumentException;
use JsonSerializable;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function Safe\base64_decode;
use function Safe\sprintf;
use Throwable;
use Webauthn\TrustPath\TrustPath;
use Webauthn\TrustPath\TrustPathLoader;
/**
* @see https://www.w3.org/TR/webauthn/#iface-pkcredential
*/
class PublicKeyCredentialSource implements JsonSerializable
{
/**
* @var string
*/
protected $publicKeyCredentialId;
/**
* @var string
*/
protected $type;
/**
* @var string[]
*/
protected $transports;
/**
* @var string
*/
protected $attestationType;
/**
* @var TrustPath
*/
protected $trustPath;
/**
* @var UuidInterface
*/
protected $aaguid;
/**
* @var string
*/
protected $credentialPublicKey;
/**
* @var string
*/
protected $userHandle;
/**
* @var int
*/
protected $counter;
/**
* @var array|null
*/
protected $otherUI;
/**
* @param string[] $transports
*/
public function __construct(string $publicKeyCredentialId, string $type, array $transports, string $attestationType, TrustPath $trustPath, UuidInterface $aaguid, string $credentialPublicKey, string $userHandle, int $counter, ?array $otherUI = null)
{
$this->publicKeyCredentialId = $publicKeyCredentialId;
$this->type = $type;
$this->transports = $transports;
$this->aaguid = $aaguid;
$this->credentialPublicKey = $credentialPublicKey;
$this->userHandle = $userHandle;
$this->counter = $counter;
$this->attestationType = $attestationType;
$this->trustPath = $trustPath;
$this->otherUI = $otherUI;
}
public function getPublicKeyCredentialId(): string
{
return $this->publicKeyCredentialId;
}
public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor
{
return new PublicKeyCredentialDescriptor(
$this->type,
$this->publicKeyCredentialId,
$this->transports
);
}
public function getAttestationType(): string
{
return $this->attestationType;
}
public function getTrustPath(): TrustPath
{
return $this->trustPath;
}
public function getAttestedCredentialData(): AttestedCredentialData
{
return new AttestedCredentialData(
$this->aaguid,
$this->publicKeyCredentialId,
$this->credentialPublicKey
);
}
public function getType(): string
{
return $this->type;
}
/**
* @return string[]
*/
public function getTransports(): array
{
return $this->transports;
}
public function getAaguid(): UuidInterface
{
return $this->aaguid;
}
public function getCredentialPublicKey(): string
{
return $this->credentialPublicKey;
}
public function getUserHandle(): string
{
return $this->userHandle;
}
public function getCounter(): int
{
return $this->counter;
}
public function setCounter(int $counter): void
{
$this->counter = $counter;
}
public function getOtherUI(): ?array
{
return $this->otherUI;
}
public function setOtherUI(?array $otherUI): self
{
$this->otherUI = $otherUI;
return $this;
}
/**
* @param mixed[] $data
*/
public static function createFromArray(array $data): self
{
$keys = array_keys(get_class_vars(self::class));
foreach ($keys as $key) {
if ('otherUI' === $key) {
continue;
}
Assertion::keyExists($data, $key, sprintf('The parameter "%s" is missing', $key));
}
switch (true) {
case 36 === mb_strlen($data['aaguid'], '8bit'):
$uuid = Uuid::fromString($data['aaguid']);
break;
default: // Kept for compatibility with old format
$decoded = base64_decode($data['aaguid'], true);
$uuid = Uuid::fromBytes($decoded);
}
try {
return new self(
Base64Url::decode($data['publicKeyCredentialId']),
$data['type'],
$data['transports'],
$data['attestationType'],
TrustPathLoader::loadTrustPath($data['trustPath']),
$uuid,
Base64Url::decode($data['credentialPublicKey']),
Base64Url::decode($data['userHandle']),
$data['counter'],
$data['otherUI'] ?? null
);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unable to load the data', $throwable->getCode(), $throwable);
}
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
return [
'publicKeyCredentialId' => Base64Url::encode($this->publicKeyCredentialId),
'type' => $this->type,
'transports' => $this->transports,
'attestationType' => $this->attestationType,
'trustPath' => $this->trustPath->jsonSerialize(),
'aaguid' => $this->aaguid->toString(),
'credentialPublicKey' => Base64Url::encode($this->credentialPublicKey),
'userHandle' => Base64Url::encode($this->userHandle),
'counter' => $this->counter,
'otherUI' => $this->otherUI,
];
}
}

View file

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
interface PublicKeyCredentialSourceRepository
{
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource;
/**
* @return PublicKeyCredentialSource[]
*/
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array;
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void;
}

View file

@ -1,87 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function Safe\base64_decode;
use function Safe\json_decode;
class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity
{
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $displayName;
public function __construct(string $name, string $id, string $displayName, ?string $icon = null)
{
parent::__construct($name, $icon);
Assertion::maxLength($id, 64, 'User ID max length is 64 bytes', 'id', '8bit');
$this->id = $id;
$this->displayName = $displayName;
}
public function getId(): string
{
return $this->id;
}
public function getDisplayName(): string
{
return $this->displayName;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'name', 'Invalid input. "name" is missing.');
Assertion::keyExists($json, 'id', 'Invalid input. "id" is missing.');
Assertion::keyExists($json, 'displayName', 'Invalid input. "displayName" is missing.');
$id = base64_decode($json['id'], true);
return new self(
$json['name'],
$id,
$json['displayName'],
$json['icon'] ?? null
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = parent::jsonSerialize();
$json['id'] = base64_encode($this->id);
$json['displayName'] = $this->displayName;
return $json;
}
}

View file

@ -1,351 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use Cose\Algorithm\Algorithm;
use Cose\Algorithm\ManagerFactory;
use Cose\Algorithm\Signature\ECDSA;
use Cose\Algorithm\Signature\EdDSA;
use Cose\Algorithm\Signature\RSA;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\RS256;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport;
use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AttestationStatement\PackedAttestationStatementSupport;
use Webauthn\AttestationStatement\TPMAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\Counter\CounterChecker;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\TokenBinding\IgnoreTokenBindingHandler;
use Webauthn\TokenBinding\TokenBindingHandler;
class Server
{
/**
* @var int
*/
public $timeout = 60000;
/**
* @var int
*/
public $challengeSize = 32;
/**
* @var PublicKeyCredentialRpEntity
*/
private $rpEntity;
/**
* @var ManagerFactory
*/
private $coseAlgorithmManagerFactory;
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSourceRepository;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var string[]
*/
private $selectedAlgorithms;
/**
* @var MetadataStatementRepository|null
*/
private $metadataStatementRepository;
/**
* @var ClientInterface|null
*/
private $httpClient;
/**
* @var string|null
*/
private $googleApiKey;
/**
* @var RequestFactoryInterface|null
*/
private $requestFactory;
/**
* @var CounterChecker|null
*/
private $counterChecker;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var string[]
*/
private $securedRelyingPartyId = [];
public function __construct(PublicKeyCredentialRpEntity $relyingParty, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, ?MetadataStatementRepository $metadataStatementRepository = null)
{
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatementRepository".', E_USER_DEPRECATED);
}
$this->rpEntity = $relyingParty;
$this->logger = new NullLogger();
$this->coseAlgorithmManagerFactory = new ManagerFactory();
$this->coseAlgorithmManagerFactory->add('RS1', new RSA\RS1());
$this->coseAlgorithmManagerFactory->add('RS256', new RSA\RS256());
$this->coseAlgorithmManagerFactory->add('RS384', new RSA\RS384());
$this->coseAlgorithmManagerFactory->add('RS512', new RSA\RS512());
$this->coseAlgorithmManagerFactory->add('PS256', new RSA\PS256());
$this->coseAlgorithmManagerFactory->add('PS384', new RSA\PS384());
$this->coseAlgorithmManagerFactory->add('PS512', new RSA\PS512());
$this->coseAlgorithmManagerFactory->add('ES256', new ECDSA\ES256());
$this->coseAlgorithmManagerFactory->add('ES256K', new ECDSA\ES256K());
$this->coseAlgorithmManagerFactory->add('ES384', new ECDSA\ES384());
$this->coseAlgorithmManagerFactory->add('ES512', new ECDSA\ES512());
$this->coseAlgorithmManagerFactory->add('Ed25519', new EdDSA\Ed25519());
$this->selectedAlgorithms = ['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512', 'Ed25519'];
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
$this->tokenBindingHandler = new IgnoreTokenBindingHandler();
$this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
$this->metadataStatementRepository = $metadataStatementRepository;
}
public function setMetadataStatementRepository(MetadataStatementRepository $metadataStatementRepository): self
{
$this->metadataStatementRepository = $metadataStatementRepository;
return $this;
}
/**
* @param string[] $selectedAlgorithms
*/
public function setSelectedAlgorithms(array $selectedAlgorithms): self
{
$this->selectedAlgorithms = $selectedAlgorithms;
return $this;
}
public function setTokenBindingHandler(TokenBindingHandler $tokenBindingHandler): self
{
$this->tokenBindingHandler = $tokenBindingHandler;
return $this;
}
public function addAlgorithm(string $alias, Algorithm $algorithm): self
{
$this->coseAlgorithmManagerFactory->add($alias, $algorithm);
$this->selectedAlgorithms[] = $alias;
$this->selectedAlgorithms = array_unique($this->selectedAlgorithms);
return $this;
}
public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): self
{
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
return $this;
}
/**
* @param string[] $securedRelyingPartyId
*/
public function setSecuredRelyingPartyId(array $securedRelyingPartyId): self
{
Assertion::allString($securedRelyingPartyId, 'Invalid list. Shall be a list of strings');
$this->securedRelyingPartyId = $securedRelyingPartyId;
return $this;
}
/**
* @param PublicKeyCredentialDescriptor[] $excludedPublicKeyDescriptors
*/
public function generatePublicKeyCredentialCreationOptions(PublicKeyCredentialUserEntity $userEntity, ?string $attestationMode = null, array $excludedPublicKeyDescriptors = [], ?AuthenticatorSelectionCriteria $criteria = null, ?AuthenticationExtensionsClientInputs $extensions = null): PublicKeyCredentialCreationOptions
{
$coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms);
$publicKeyCredentialParametersList = [];
foreach ($coseAlgorithmManager->all() as $algorithm) {
$publicKeyCredentialParametersList[] = new PublicKeyCredentialParameters(
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
$algorithm::identifier()
);
}
$criteria = $criteria ?? new AuthenticatorSelectionCriteria();
$extensions = $extensions ?? new AuthenticationExtensionsClientInputs();
$challenge = random_bytes($this->challengeSize);
return PublicKeyCredentialCreationOptions::create(
$this->rpEntity,
$userEntity,
$challenge,
$publicKeyCredentialParametersList
)
->excludeCredentials($excludedPublicKeyDescriptors)
->setAuthenticatorSelection($criteria)
->setAttestation($attestationMode ?? PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE)
->setExtensions($extensions)
->setTimeout($this->timeout)
;
}
/**
* @param PublicKeyCredentialDescriptor[] $allowedPublicKeyDescriptors
*/
public function generatePublicKeyCredentialRequestOptions(?string $userVerification = null, array $allowedPublicKeyDescriptors = [], ?AuthenticationExtensionsClientInputs $extensions = null): PublicKeyCredentialRequestOptions
{
return PublicKeyCredentialRequestOptions::create(random_bytes($this->challengeSize))
->setRpId($this->rpEntity->getId())
->setUserVerification($userVerification ?? PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED)
->allowCredentials($allowedPublicKeyDescriptors)
->setTimeout($this->timeout)
->setExtensions($extensions ?? new AuthenticationExtensionsClientInputs())
;
}
public function loadAndCheckAttestationResponse(string $data, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $serverRequest): PublicKeyCredentialSource
{
$attestationStatementSupportManager = $this->getAttestationStatementSupportManager();
$attestationObjectLoader = AttestationObjectLoader::create($attestationStatementSupportManager)
->setLogger($this->logger)
;
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader)
->setLogger($this->logger)
;
$publicKeyCredential = $publicKeyCredentialLoader->load($data);
$authenticatorResponse = $publicKeyCredential->getResponse();
Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAttestationResponse::class, 'Not an authenticator attestation response');
$authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
$attestationStatementSupportManager,
$this->publicKeyCredentialSourceRepository,
$this->tokenBindingHandler,
$this->extensionOutputCheckerHandler,
$this->metadataStatementRepository
);
$authenticatorAttestationResponseValidator->setLogger($this->logger);
return $authenticatorAttestationResponseValidator->check($authenticatorResponse, $publicKeyCredentialCreationOptions, $serverRequest, $this->securedRelyingPartyId);
}
public function loadAndCheckAssertionResponse(string $data, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ?PublicKeyCredentialUserEntity $userEntity, ServerRequestInterface $serverRequest): PublicKeyCredentialSource
{
$attestationStatementSupportManager = $this->getAttestationStatementSupportManager();
$attestationObjectLoader = AttestationObjectLoader::create($attestationStatementSupportManager)
->setLogger($this->logger)
;
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader)
->setLogger($this->logger)
;
$publicKeyCredential = $publicKeyCredentialLoader->load($data);
$authenticatorResponse = $publicKeyCredential->getResponse();
Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAssertionResponse::class, 'Not an authenticator assertion response');
$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
$this->publicKeyCredentialSourceRepository,
$this->tokenBindingHandler,
$this->extensionOutputCheckerHandler,
$this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms),
$this->counterChecker
);
$authenticatorAssertionResponseValidator->setLogger($this->logger);
return $authenticatorAssertionResponseValidator->check(
$publicKeyCredential->getRawId(),
$authenticatorResponse,
$publicKeyCredentialRequestOptions,
$serverRequest,
null !== $userEntity ? $userEntity->getId() : null,
$this->securedRelyingPartyId
);
}
public function setCounterChecker(CounterChecker $counterChecker): self
{
$this->counterChecker = $counterChecker;
return $this;
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function enforceAndroidSafetyNetVerification(ClientInterface $client, string $apiKey, RequestFactoryInterface $requestFactory): self
{
$this->httpClient = $client;
$this->googleApiKey = $apiKey;
$this->requestFactory = $requestFactory;
return $this;
}
private function getAttestationStatementSupportManager(): AttestationStatementSupportManager
{
$attestationStatementSupportManager = new AttestationStatementSupportManager();
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
$attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport());
if (class_exists(RS256::class) && class_exists(JWKFactory::class)) {
$androidSafetyNetAttestationStatementSupport = new AndroidSafetyNetAttestationStatementSupport();
if (null !== $this->httpClient && null !== $this->googleApiKey && null !== $this->requestFactory) {
$androidSafetyNetAttestationStatementSupport
->enableApiVerification($this->httpClient, $this->googleApiKey, $this->requestFactory)
->setLeeway(2000)
->setMaxAge(60000)
;
}
$attestationStatementSupportManager->add($androidSafetyNetAttestationStatementSupport);
}
$attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport());
$attestationStatementSupportManager->add(new TPMAttestationStatementSupport());
$coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms);
$attestationStatementSupportManager->add(new PackedAttestationStatementSupport($coseAlgorithmManager));
return $attestationStatementSupportManager;
}
}

View file

@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use CBOR\Stream;
use function Safe\fclose;
use function Safe\fopen;
use function Safe\fread;
use function Safe\fwrite;
use function Safe\rewind;
use function Safe\sprintf;
final class StringStream implements Stream
{
/**
* @var resource
*/
private $data;
/**
* @var int
*/
private $length;
/**
* @var int
*/
private $totalRead = 0;
public function __construct(string $data)
{
$this->length = mb_strlen($data, '8bit');
$resource = fopen('php://memory', 'rb+');
fwrite($resource, $data);
rewind($resource);
$this->data = $resource;
}
public function read(int $length): string
{
if (0 === $length) {
return '';
}
$read = fread($this->data, $length);
$bytesRead = mb_strlen($read, '8bit');
Assertion::length($read, $length, sprintf('Out of range. Expected: %d, read: %d.', $length, $bytesRead), null, '8bit');
$this->totalRead += $bytesRead;
return $read;
}
public function close(): void
{
fclose($this->data);
}
public function isEOF(): bool
{
return $this->totalRead === $this->length;
}
}

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TokenBinding;
use Psr\Http\Message\ServerRequestInterface;
final class IgnoreTokenBindingHandler implements TokenBindingHandler
{
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
{
//Does nothing
}
}

View file

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TokenBinding;
use Assert\Assertion;
use Psr\Http\Message\ServerRequestInterface;
final class SecTokenBindingHandler implements TokenBindingHandler
{
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
{
if (TokenBinding::TOKEN_BINDING_STATUS_PRESENT !== $tokenBinding->getStatus()) {
return;
}
Assertion::true($request->hasHeader('Sec-Token-Binding'), 'The header parameter "Sec-Token-Binding" is missing.');
$tokenBindingIds = $request->getHeader('Sec-Token-Binding');
Assertion::count($tokenBindingIds, 1, 'The header parameter "Sec-Token-Binding" is invalid.');
$tokenBindingId = reset($tokenBindingIds);
Assertion::eq($tokenBindingId, $tokenBinding->getId(), 'The header parameter "Sec-Token-Binding" is invalid.');
}
}

View file

@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TokenBinding;
use function array_key_exists;
use Assert\Assertion;
use Base64Url\Base64Url;
use function Safe\sprintf;
class TokenBinding
{
public const TOKEN_BINDING_STATUS_PRESENT = 'present';
public const TOKEN_BINDING_STATUS_SUPPORTED = 'supported';
public const TOKEN_BINDING_STATUS_NOT_SUPPORTED = 'not-supported';
/**
* @var string
*/
private $status;
/**
* @var string|null
*/
private $id;
public function __construct(string $status, ?string $id)
{
Assertion::false(self::TOKEN_BINDING_STATUS_PRESENT === $status && null === $id, 'The member "id" is required when status is "present"');
$this->status = $status;
$this->id = $id;
}
/**
* @param mixed[] $json
*/
public static function createFormArray(array $json): self
{
Assertion::keyExists($json, 'status', 'The member "status" is required');
$status = $json['status'];
Assertion::inArray(
$status,
self::getSupportedStatus(),
sprintf('The member "status" is invalid. Supported values are: %s', implode(', ', self::getSupportedStatus()))
);
$id = array_key_exists('id', $json) ? Base64Url::decode($json['id']) : null;
return new self($status, $id);
}
public function getStatus(): string
{
return $this->status;
}
public function getId(): ?string
{
return $this->id;
}
/**
* @return string[]
*/
private static function getSupportedStatus(): array
{
return [
self::TOKEN_BINDING_STATUS_PRESENT,
self::TOKEN_BINDING_STATUS_SUPPORTED,
self::TOKEN_BINDING_STATUS_NOT_SUPPORTED,
];
}
}

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TokenBinding;
use Psr\Http\Message\ServerRequestInterface;
interface TokenBindingHandler
{
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void;
}

View file

@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TokenBinding;
use Assert\Assertion;
use Psr\Http\Message\ServerRequestInterface;
final class TokenBindingNotSupportedHandler implements TokenBindingHandler
{
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
{
Assertion::true(TokenBinding::TOKEN_BINDING_STATUS_PRESENT !== $tokenBinding->getStatus(), 'Token binding not supported.');
}
}

View file

@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TrustPath;
use Assert\Assertion;
final class CertificateTrustPath implements TrustPath
{
/**
* @var string[]
*/
private $certificates;
/**
* @param string[] $certificates
*/
public function __construct(array $certificates)
{
$this->certificates = $certificates;
}
/**
* @return string[]
*/
public function getCertificates(): array
{
return $this->certificates;
}
/**
* {@inheritdoc}
*/
public static function createFromArray(array $data): TrustPath
{
Assertion::keyExists($data, 'x5c', 'The trust path type is invalid');
return new CertificateTrustPath($data['x5c']);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
return [
'type' => self::class,
'x5c' => $this->certificates,
];
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TrustPath;
use Assert\Assertion;
final class EcdaaKeyIdTrustPath implements TrustPath
{
/**
* @var string
*/
private $ecdaaKeyId;
public function __construct(string $ecdaaKeyId)
{
$this->ecdaaKeyId = $ecdaaKeyId;
}
public function getEcdaaKeyId(): string
{
return $this->ecdaaKeyId;
}
/**
* @return string[]
*/
public function jsonSerialize(): array
{
return [
'type' => self::class,
'ecdaaKeyId' => $this->ecdaaKeyId,
];
}
/**
* {@inheritdoc}
*/
public static function createFromArray(array $data): TrustPath
{
Assertion::keyExists($data, 'ecdaaKeyId', 'The trust path type is invalid');
return new EcdaaKeyIdTrustPath($data['ecdaaKeyId']);
}
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TrustPath;
final class EmptyTrustPath implements TrustPath
{
/**
* @return string[]
*/
public function jsonSerialize(): array
{
return [
'type' => self::class,
];
}
/**
* {@inheritdoc}
*/
public static function createFromArray(array $data): TrustPath
{
return new EmptyTrustPath();
}
}

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TrustPath;
use JsonSerializable;
interface TrustPath extends JsonSerializable
{
/**
* @param mixed[] $data
*/
public static function createFromArray(array $data): self;
}

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\TrustPath;
use function array_key_exists;
use Assert\Assertion;
use function in_array;
use InvalidArgumentException;
use function Safe\class_implements;
use function Safe\sprintf;
abstract class TrustPathLoader
{
/**
* @param mixed[] $data
*/
public static function loadTrustPath(array $data): TrustPath
{
Assertion::keyExists($data, 'type', 'The trust path type is missing');
$type = $data['type'];
$oldTypes = self::oldTrustPathTypes();
switch (true) {
case array_key_exists($type, $oldTypes):
return $oldTypes[$type]::createFromArray($data);
case class_exists($type):
$implements = class_implements($type);
if (in_array(TrustPath::class, $implements, true)) {
return $type::createFromArray($data);
}
// no break
default:
throw new InvalidArgumentException(sprintf('The trust path type "%s" is not supported', $data['type']));
}
}
/**
* @return string[]
*/
private static function oldTrustPathTypes(): array
{
return [
'empty' => EmptyTrustPath::class,
'ecdaa_key_id' => EcdaaKeyIdTrustPath::class,
'x5c' => CertificateTrustPath::class,
];
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace Webauthn;
use CBOR\ByteStringObject;
use CBOR\MapItem;
use CBOR\MapObject;
use CBOR\NegativeIntegerObject;
use CBOR\UnsignedIntegerObject;
class U2FPublicKey
{
public static function isU2FKey($publicKey): bool
{
return $publicKey[0] === "\x04";
}
public static function createCOSEKey($publicKey): string
{
$mapObject = new MapObject([
1 => MapItem::create(
new UnsignedIntegerObject(1, null),
new UnsignedIntegerObject(2, null)
),
3 => MapItem::create(
new UnsignedIntegerObject(3, null),
new NegativeIntegerObject(6, null)
),
-1 => MapItem::create(
new NegativeIntegerObject(0, null),
new UnsignedIntegerObject(1, null)
),
-2 => MapItem::create(
new NegativeIntegerObject(1, null),
new ByteStringObject(substr($publicKey, 1, 32))
),
-3 => MapItem::create(
new NegativeIntegerObject(2, null),
new ByteStringObject(substr($publicKey, 33))
),
]);
return $mapObject->__toString();
}
}

View file

@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\Util;
use Cose\Algorithm\Signature\ECDSA;
use Cose\Algorithm\Signature\Signature;
/**
* This class fixes the signature of the ECDSA based algorithms.
*
* @internal
*
* @see https://www.w3.org/TR/webauthn/#signature-attestation-types
*/
abstract class CoseSignatureFixer
{
public static function fix(string $signature, Signature $algorithm): string
{
switch ($algorithm::identifier()) {
case ECDSA\ES256K::ID:
case ECDSA\ES256::ID:
if (64 === mb_strlen($signature, '8bit')) {
return $signature;
}
return ECDSA\ECSignature::fromAsn1($signature, 64); //TODO: fix this hardcoded value by adding a dedicated method for the algorithms
case ECDSA\ES384::ID:
if (96 === mb_strlen($signature, '8bit')) {
return $signature;
}
return ECDSA\ECSignature::fromAsn1($signature, 96);
case ECDSA\ES512::ID:
if (132 === mb_strlen($signature, '8bit')) {
return $signature;
}
return ECDSA\ECSignature::fromAsn1($signature, 132);
}
return $signature;
}
}