Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mdsDownloadCommand.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/console mds:download
76 changes: 76 additions & 0 deletions src/Command/MdsDownloadCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Surfnet\Webauthn\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
name: 'mds:download',
description: 'Downloads MDS data',

)]
class MdsDownloadCommand extends Command
{
const MDS_METADATA_URL = 'https://mds3.fidoalliance.org/blob.jwt';

public function __construct(
private readonly string $jwtMdsBlobFileName,
) {
parent::__construct();
}

protected function configure(): void
{
$this->setHelp('Downloads MDS data from "' . self::MDS_METADATA_URL . '" and saves it to "' . $this->jwtMdsBlobFileName . '".');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$mdsFile = $this->jwtMdsBlobFileName;

// Get directory of mdsFile
$mdsDir = dirname($mdsFile);

if (!is_dir($mdsDir) || !is_writable($mdsDir)) {
$output->writeln('MDS directory does not exist or is not writeable');
return Command::FAILURE;
}
$output->writeln("Starting MDS download from " . self::MDS_METADATA_URL );

// Create temporary directory
$tempDir = sys_get_temp_dir();
$tempMdsFile = $tempDir . '/blob.jwt';

try {
// Use Guzzle to download the MDS metadata to $tempDir
$client = new \GuzzleHttp\Client(
['allow_redirects' => true]
);
$response = $client->request('GET', self::MDS_METADATA_URL,
['sink' => $tempMdsFile]);

$status = $response->getStatusCode();
if ($status != 200) {
$output->writeln("MDS download failed with HTTP status $status");
return Command::FAILURE;
}

// Move mds file into place
rename($tempMdsFile, $mdsFile);

$output->writeln('Wrote MDS file to ' . $mdsFile);
return Command::SUCCESS;
} finally {
// Cleanup temp file if exists, temp dir is OS business.
if (file_exists($tempMdsFile)) {
unlink($tempMdsFile);
}
}

}
}
127 changes: 127 additions & 0 deletions src/Command/MdsLookupCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace Surfnet\Webauthn\Command;

use CBOR\Decoder;
use DateTime;
use Surfnet\Webauthn\Repository\MetadataStatementRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator;
use Webauthn\StringStream;

#[AsCommand(
name: 'mds:lookup',
description: 'Lookup an aaguid in the MDS',
)]
class MdsLookupCommand extends Command
{
public function __construct(
private readonly string $jwtMdsBlobFileName,
private readonly string $jwtMdsRootCertFileName,
private readonly string $mdsCacheDir,
private readonly HttpClientInterface $client,
private readonly SerializerInterface $serializer
)
{
parent::__construct();
}

protected function configure(): void
{
$this->setHelp(
"Lookup an aaguid in the MDS data in '" . $this->jwtMdsBlobFileName . "'.\n" .
"You can download (new) MDS data from FIDO using the 'mds:download' console command.\n\n"
);

$this->addArgument('aaguid', InputArgument::REQUIRED, 'aaguid to lookup. Example: ee041bce-25e5-4cdb-8f86-897fd6418464');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$mdsFile = $this->jwtMdsBlobFileName;
$rootCertFile = $this->jwtMdsRootCertFileName;
$httpClient = $this->client;
$serializer = $this->serializer;

if (!file_exists($mdsFile) || !is_readable($mdsFile)) {
$output->writeln("MDS file does not exist or is not readable. File: $mdsFile");
return Command::FAILURE;
}

if (!file_exists($rootCertFile) || !is_readable($rootCertFile)) {
$output->writeln("X.509 root certificate for the MDS file does exist or is not readable. File: $mdsFile");
return Command::FAILURE;
}

// Use a temporary directory for the MDS cache as not to disturb the original cache
$mdsCacheDir = sys_get_temp_dir() . '/mds_cache';
if (!file_exists($mdsCacheDir)) {
if (!mkdir($mdsCacheDir)) {
$output->writeln("Failed to create MDS cache directory. Directory: $mdsCacheDir");
return Command::FAILURE;
}
}

try {
$output->writeln("Verifying MDS file...");

// Use the existing MetadataStatementRepository
$mdsRepo = new MetadataStatementRepository($mdsFile, $rootCertFile, $mdsCacheDir,
new PhpCertificateChainValidator($httpClient), $serializer);

$output->writeln("MDS file verified OK");

// The PEM X.509 root certificates for the authenticator from the MDS file
$rootCerts = array();

$aaguid = $input->getArgument('aaguid');
$output->writeln("Looking up metadata for AAGUID: $aaguid");
if (!$mdsRepo->has($aaguid)) {
$output->writeln("AAGUID not found in MDS");
return Command::FAILURE;
}
$out = $mdsRepo->get($aaguid);
$output->writeln( json_encode($out, JSON_PRETTY_PRINT) );

$output->writeln("\nAttestation root certificates:");
$nr=0;
foreach ($out->attestationRootCertificates as $cert) {
$nr++;
$output->writeln("#$nr:");
$certPEM = Utils::base64ToPEMCert($cert);
$cert_parsed=openssl_x509_parse($certPEM);
if (!$cert_parsed) {
$output->writeln("Failed to parse certificate:");
$output->writeln($certPEM);
continue;
}
$rootCerts[] = $certPEM;

$output->writeln(Utils::X509toString($cert_parsed));
$certDER=base64_decode($cert);
$sha1=hash('sha1',$certDER,false);
$sha256=hash('sha256',$certDER,false);
$output->writeln("Fingerprint: SHA-1=$sha1; SHA-256=$sha256");
$output->writeln("");
}

return Command::SUCCESS;

} finally {
$output->writeln("Cleaning up temporary cache directory...");
Utils::recursivelyRemoveDirectory($mdsCacheDir);
$output->writeln("Done.");
}
}
}
Loading
Loading