Skip to content

Instantly share code, notes, and snippets.

@Nevercold
Last active January 25, 2023 15:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nevercold/f9b7527846c7db72c6bccc7a94867fcb to your computer and use it in GitHub Desktop.
Save Nevercold/f9b7527846c7db72c6bccc7a94867fcb to your computer and use it in GitHub Desktop.
<?php
// just the basics from the docs
namespace hmcsw\service\authorization\auth\twoFactor\types\fido2;
use Cose\Algorithms;
use Cose\Algorithm\Manager;
use Lcobucci\Clock\SystemClock;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\Clock\NativeClock;
use Webauthn\PublicKeyCredentialLoader;
use Cose\Algorithm\Signature\RSA\PS256;
use Cose\Algorithm\Signature\RSA\PS384;
use Cose\Algorithm\Signature\RSA\PS512;
use Cose\Algorithm\Signature\RSA\RS256;
use Cose\Algorithm\Signature\RSA\RS384;
use Cose\Algorithm\Signature\RSA\RS512;
use hmcsw\service\config\ConfigService;
use hmcsw\routing\routes\status\status;
use Webauthn\PublicKeyCredentialRpEntity;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\ECDSA\ES384;
use Cose\Algorithm\Signature\ECDSA\ES512;
use Cose\Algorithm\Signature\EdDSA\Ed256;
use Cose\Algorithm\Signature\EdDSA\Ed512;
use Cose\Algorithm\Signature\ECDSA\ES256K;
use Webauthn\PublicKeyCredentialParameters;
use Nyholm\Psr7Server\ServerRequestCreator;
use Webauthn\TokenBinding\IgnoreTokenBindingHandler;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\TPMAttestationStatementSupport;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AttestationStatement\AppleAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AttestationStatement\PackedAttestationStatementSupport;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport;
use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport;
class Fido2
{
protected static function rpEntity (): PublicKeyCredentialRpEntity
{
return PublicKeyCredentialRpEntity::create(ConfigService::getConfig()['name'], self::getRpId());
}
protected static function getRpId (): string
{
return ConfigService::getUrl("without_sub");
}
protected static function parametersList (): array
{
return [PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256K),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED512),];
}
protected static function publicKeyCredentialLoader (): PublicKeyCredentialLoader
{
return PublicKeyCredentialLoader::create(self::attestationObjectLoader());
}
protected static function attestationObjectLoader (): AttestationObjectLoader
{
return new AttestationObjectLoader(self::attestationStatementSupportManager());
}
protected static function attestationStatementSupportManager (): AttestationStatementSupportManager
{
$clock = SystemClock::fromSystemTimezone();
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$attestationStatementSupportManager->add(FidoU2FAttestationStatementSupport::create());
$attestationStatementSupportManager->add(AppleAttestationStatementSupport::create());
$androidSafetyNetAttestationStatementSupport = AndroidSafetyNetAttestationStatementSupport::create()
// ->enableApiVerification( $psr18Client, $googleApiKey, $psr17RequestFactory) // TODO
;
$attestationStatementSupportManager->add($androidSafetyNetAttestationStatementSupport);
$attestationStatementSupportManager->add(AndroidKeyAttestationStatementSupport::create());
$attestationStatementSupportManager->add(TPMAttestationStatementSupport::create($clock));
$attestationStatementSupportManager->add(PackedAttestationStatementSupport::create(self::coseAlgorithmManager()));
return $attestationStatementSupportManager;
}
protected static function coseAlgorithmManager(): Manager
{
$coseAlgorithmManager = new Manager();
$coseAlgorithmManager->add(new ES256());
$coseAlgorithmManager->add(new RS256());
return $coseAlgorithmManager;
}
protected static function serverRequestCreator (): ServerRequestCreator
{
$psr17Factory = self::Psr17Factory();
return new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
}
public static function Psr17Factory():Psr17Factory
{
return new Psr17Factory();
}
protected static function authenticatorAssertionResponseValidator (): AuthenticatorAssertionResponseValidator
{
return AuthenticatorAssertionResponseValidator::create(self::publicKeyCredentialSourceRepository(), // The Credential Repository service
null, // The token binding handler | removed deprecated
self::extensionOutputCheckerHandler(), // The extension output checker handler
self::algorithmManager());
}
protected static function publicKeyCredentialSourceRepository (): PublicKeyCredentialSourceRepository
{
return new PublicKeyCredentialSourceRepository();
}
/**
* @deprecated
* @return IgnoreTokenBindingHandler
*/
protected static function tokenBindingHandler (): IgnoreTokenBindingHandler
{
return IgnoreTokenBindingHandler::create();
}
protected static function extensionOutputCheckerHandler (): ExtensionOutputCheckerHandler
{
return ExtensionOutputCheckerHandler::create();
}
protected static function algorithmManager (): Manager
{
return Manager::create()->add(ES256::create(), ES256K::create(), ES384::create(), ES512::create(),
RS256::create(), RS384::create(), RS512::create(),
PS256::create(), PS384::create(), PS512::create(),
Ed256::create(), Ed512::create());
}
protected static function authenticatorAttestationResponseValidator (): AuthenticatorAttestationResponseValidator
{
return AuthenticatorAttestationResponseValidator::create(self::attestationStatementSupportManager(), self::publicKeyCredentialSourceRepository(), null, self::extensionOutputCheckerHandler());
}
protected static function publicKeyCredentialUserEntityRepository (): PublicKeyCredentialUserEntityRepository
{
return new PublicKeyCredentialUserEntityRepository();
}
protected static function createChallengeId (int $length): string
{
return random_bytes($length);
}
}
<?php
namespace hmcsw\service\authorization\auth\twoFactor\types\fido2;
use hmcsw\hmcsw4;
use Firebase\JWT\JWT;
use hmcsw\utils\UnitUtil;
use hmcsw\utils\ByteBuffer;
use hmcsw\service\cache\CacheService;
use Webauthn\Exception\WebauthnException;
use Webauthn\MetadataService\Statement\MetadataStatement;
class MetadataStatementRepository extends Fido2 implements \Webauthn\MetadataService\MetadataStatementRepository
{
public function findOneByAAGUID (string $aaguid): ?MetadataStatement
{
foreach (self::getMetadataService() as $metadata) {
if ($metadata['aaguid'] === $aaguid) {
return MetadataStatement::createFromArray($metadata['metadataStatement']);
}
}
return null;
}
protected static function getMetadataService (): array
{
return self::queryFidoMetaDataService();
}
public static function queryFidoMetaDataService (): array
{
$cache = CacheService::get("metaDataStatement", false, true);
if($cache['success']){
return $cache['response'];
}
$devices = [];
$url = 'https://mds.fidoalliance.org/';
$raw = null;
if (\function_exists('curl_init')) {
$ch = \curl_init($url);
\curl_setopt($ch, CURLOPT_HEADER, false);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
\curl_setopt($ch, CURLOPT_USERAGENT, hmcsw4::getUserAgent());
$raw = \curl_exec($ch);
\curl_close($ch);
} else {
$raw = \file_get_contents($url);
}
if (!\is_string($raw)) {
throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service');
}
$jwt = \explode('.', $raw);
if (\count($jwt) !== 3) {
throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service');
}
[$header, $payload, $hash] = $jwt;
$payload = ByteBuffer::fromBase64Url($payload)->getJson();
$count = 0;
if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) {
foreach ($payload->entries as $entry) {
if(isset($entry->aaguid)){
$devices[$entry->aaguid] = $entry;
}
}
}
CacheService::save("metaDataStatement", json_encode($devices), false, time()+(60*60*24));
return json_decode(json_encode($devices), true);
}
}
<?php
namespace hmcsw\service\authorization\auth\twoFactor\types\fido2;
use Throwable;
use http\Client;
use hmcsw\utils\UnitUtil;
use hmcsw\service\Services;
use hmcsw\service\cache\CacheService;
use hmcsw\exception\InternalException;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\Exception\InvalidDataException;
use hmcsw\service\authorization\TokenService;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator;
class registerFido2 extends Fido2
{
/**
*
* Creates the Option Json String to start Fido2 Authentication
*
* @param $accessToken
* @return PublicKeyCredentialCreationOptions|array
* @throws InternalException|InvalidDataException
*/
public static function option ($accessToken): PublicKeyCredentialCreationOptions|array
{
$input = json_decode(file_get_contents('php://input'), true);
$keyName = $input['keyName'] ?? "Fido2 Key"; // name for key, set by user
$mode = $input['mode'] ?? "integrated"; // physical or integrated. User select this on setup.
$token = TokenService::getAccessToken($accessToken); // user token validation, ignore this
if (!$token['success']) return $token;
$user_id = $token['response']['user_id'];
$PublicKeyCredentialUserEntityRepository = new PublicKeyCredentialUserEntityRepository();
$PublicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
$userEntity = $PublicKeyCredentialUserEntityRepository->findWebauthnUserByUserId($user_id); // create user entity obj
$credentialSources = $PublicKeyCredentialSourceRepository->findAllForUserEntity($userEntity);
$excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
return $credential->getPublicKeyCredentialDescriptor();
}, $credentialSources);
$challenge = self::createChallengeId(16);
if($mode == "physical") {
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create()->setUserVerification(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED)
->setResidentKey(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED)
->setAuthenticatorAttachment(AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM); <-- Difference
} else {
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create()->setUserVerification(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED)
->setResidentKey(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED)
->setAuthenticatorAttachment(AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM); <-- Difference, If this is set to Cross_platform, Windows Hello cannot be added
};
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create(self::rpEntity(), $userEntity, $challenge, self::parametersList())->setTimeout(60000)->excludeCredentials(...$excludeCredentials)->setAuthenticatorSelection($authenticatorSelectionCriteria);
$publicKeyCredentialCreationOptions->setAttestation(PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT);
CacheService::delete(UnitUtil::base64_encode("fido2-accessToken-" . $accessToken));
CacheService::save(UnitUtil::base64_encode("fido2-accessToken-" . $accessToken), json_encode(["mode" => $mode, "keyName" => $keyName,
"publicKeyCredentialCreationOptions" => $publicKeyCredentialCreationOptions,
"userEntity" => $userEntity]), false, time() + 3600); // save in cache.
return $publicKeyCredentialCreationOptions;
}
/**
*
* Check the Response from the Fido2 Authentication
* On success: key will add to user
*
* @param string $response
* @param string $accessToken
* @return array
*/
public static function action (string $response, string $accessToken): array
{
if($response == null){
return ["success" => false, "response" => ["error_code" => 400, "error_message" => "wrong request"]];
}
$token = TokenService::getAccessToken($accessToken);
if (!$token['success']) return $token;
$user_id = $token['response']['user_id']; // valid token again, ignore
$cache = CacheService::get(UnitUtil::base64_encode("fido2-accessToken-" . $accessToken));
if (!$cache['success']) return $cache;
$cache = $cache['response'];
$keyName = $cache['keyName'];
$publicKeyCredentialCreationOptions = $cache['publicKeyCredentialCreationOptions'];
try {
$publicKeyCredential = self::publicKeyCredentialLoader()->load($response);
$authenticatorAttestationResponse = $publicKeyCredential->getResponse();
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
return ["success" => false, "response" => ["error_code" => 400, "error_message" => "wrong request"]];
}
print_r($authenticatorAttestationResponse);
$serverRequest = self::serverRequestCreator()->fromGlobals();
$publicKeyCredentialSource = self::authenticatorAttestationResponseValidator()
->enableMetadataStatementSupport(new MetadataStatementRepository(), new StatusReportRepository(), new PhpCertificateChainValidator(new \GuzzleHttp\Client(), Fido2::Psr17Factory()))
->check($authenticatorAttestationResponse,
PublicKeyCredentialCreationOptions::createFromArray($publicKeyCredentialCreationOptions), $serverRequest); <-- Failed in check
CacheService::delete(UnitUtil::base64_encode("fido2-accessToken-" . $accessToken)); // ignore, delete cache
self::addKey($publicKeyCredentialSource, $user_id, $keyName); // save key
return ["success" => true, "code" => 200]; // response to website
} catch (Throwable $exception) {
return ["success" => false,
"response" => ["error_code" => $exception->getCode(),
"error_message" => $exception->getMessage(),
"error_response" => $exception->getTrace()]];
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment