Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
Webauthn support (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
Firehed committed Oct 26, 2021
1 parent 58a97aa commit e2b7d9e
Show file tree
Hide file tree
Showing 18 changed files with 1,133 additions and 70 deletions.
271 changes: 210 additions & 61 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
"mfa",
"security",
"u2f",
"webauthn",
"web authentication",
"webauthentication",
"yubico",
"yubikey"
],
"homepage": "https://github.com/Firehed/u2f-php",
"require": {
"php": ">=7.2"
"php": ">=7.2",
"firehed/cbor": "^0.1"
},
"require-dev": {
"phpstan/phpstan": "^0.12",
Expand Down
40 changes: 40 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@ parameters:
count: 1
path: src/ClientData.php

-
message: "#^Cannot access offset 1 on array\\|false\\.$#"
count: 2
path: src/WebAuthn/AuthenticatorData.php

-
message: "#^Property Firehed\\\\U2F\\\\WebAuthn\\\\AuthenticatorData\\:\\:\\$extensions is unused\\.$#"
count: 1
path: src/WebAuthn/AuthenticatorData.php

-
message: "#^Offset \\-2 does not exist on array\\(1 \\=\\> int, \\-1 \\=\\> int, \\?\\-2 \\=\\> string, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string, 3 \\=\\> \\-7\\)\\.$#"
count: 1
path: src/WebAuthn/RegistrationResponse.php

-
message: "#^Offset \\-3 does not exist on array\\(1 \\=\\> int, \\-1 \\=\\> int, \\?\\-2 \\=\\> string, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string, 3 \\=\\> \\-7\\)\\.$#"
count: 1
path: src/WebAuthn/RegistrationResponse.php

-
message: "#^Offset 3 does not exist on array\\(1 \\=\\> int, \\?3 \\=\\> int, \\-1 \\=\\> int, \\?\\-2 \\=\\> string, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string\\)\\.$#"
count: 1
path: src/WebAuthn/RegistrationResponse.php

-
message: "#^Method Firehed\\\\U2F\\\\FunctionsTest\\:\\:vectors\\(\\) should return array\\<array\\(string, string\\)\\> but returns array\\(array\\('', ''\\), array\\('f', 'Zg'\\), array\\('fo', 'Zm8'\\), array\\('foo', 'Zm9v'\\), array\\('foob', 'Zm9vYg'\\), array\\('fooba', 'Zm9vYmE'\\), array\\('foobar', 'Zm9vYmFy'\\), array\\(string\\|false, 'AA_BB\\-cc'\\)\\)\\.$#"
count: 1
Expand All @@ -25,3 +50,18 @@ parameters:
count: 1
path: tests/ResponseTraitTest.php

-
message: "#^Offset \\-2 does not exist on array\\(1 \\=\\> int, \\-1 \\=\\> 1, 3 \\=\\> \\-7, \\?\\-2 \\=\\> string, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string\\)\\.$#"
count: 1
path: tests/WebAuthn/AuthenticatorDataTest.php

-
message: "#^Offset \\-3 does not exist on array\\(1 \\=\\> int, \\-1 \\=\\> 1, 3 \\=\\> \\-7, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string, \\-2 \\=\\> string\\)\\.$#"
count: 1
path: tests/WebAuthn/AuthenticatorDataTest.php

-
message: "#^Offset 3 does not exist on array\\(1 \\=\\> int, \\?3 \\=\\> int, \\-1 \\=\\> int, \\?\\-2 \\=\\> string, \\?\\-3 \\=\\> string, \\?\\-4 \\=\\> string\\)\\.$#"
count: 1
path: tests/WebAuthn/AuthenticatorDataTest.php

9 changes: 8 additions & 1 deletion src/RegisterRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ class RegisterRequest implements JsonSerializable, ChallengeProvider
use ChallengeTrait;
use VersionTrait;

public function jsonSerialize()
/**
* @return array{
* version: string,
* challenge: string,
* appId: string,
* }
*/
public function jsonSerialize(): array
{
return [
"version" => $this->version,
Expand Down
11 changes: 9 additions & 2 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,21 @@ public function generateSignRequest(RegistrationInterface $reg): SignRequest
}

/**
* Wraps generateSignRequest for multiple registrations
* Wraps generateSignRequest for multiple registrations. Using this API
* ensures that all sign requests share a single challenge, which greatly
* simplifies compatibility with WebAuthn
*
* @param RegistrationInterface[] $registrations
* @return SignRequest[]
*/
public function generateSignRequests(array $registrations): array
{
return array_values(array_map([$this, 'generateSignRequest'], $registrations));
$challenge = $this->generateChallenge();
$requests = array_map([$this, 'generateSignRequest'], $registrations);
$requestsWithSameChallenge = array_map(function (SignRequest $req) use ($challenge) {
return $req->setChallenge($challenge);
}, $requests);
return array_values($requestsWithSameChallenge);
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/SignRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ class SignRequest implements JsonSerializable, ChallengeProvider, KeyHandleInter
use KeyHandleTrait;
use VersionTrait;

public function jsonSerialize()
/**
* @return array{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string,
* }
*/
public function jsonSerialize(): array
{
return [
"version" => $this->version,
Expand Down
177 changes: 177 additions & 0 deletions src/WebAuthn/AuthenticatorData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);

namespace Firehed\U2F\WebAuthn;

use BadMethodCallException;
use Firehed\CBOR\Decoder;

/**
* @phpstan-type AttestedCredentialData array{
* aaguid: string,
* credentialId: string,
* credentialPublicKey: array{
* 1: int,
* 3?: int,
* -1: int,
* -2?: string,
* -3?: string,
* -4?: string,
* }
* }
*/
class AuthenticatorData
{
/** @var bool */
private $isUserPresent;

/** @var bool */
private $isUserVerified;

/** @var string (binary) */
private $rpIdHash;

/** @var int */
private $signCount;

/**
* @var ?AttestedCredentialData Attested Credential Data
*/
private $ACD;

/** @var null RESERVED: WebAuthn Extensions */
private $extensions;

/**
* @see https://w3c.github.io/webauthn/#sec-authenticator-data
* WebAuthn 6.1
*/
public static function parse(string $bytes): AuthenticatorData
{
assert(strlen($bytes) >= 37);

$rpIdHash = substr($bytes, 0, 32);
$flags = ord(substr($bytes, 32, 1));
$UP = ($flags & 0x01) === 0x01; // bit 0
$UV = ($flags & 0x04) === 0x04; // bit 2
$AT = ($flags & 0x40) === 0x40; // bit 6
$ED = ($flags & 0x80) === 0x80; // bit 7
$signCount = unpack('N', substr($bytes, 33, 4))[1];

$authData = new AuthenticatorData();
$authData->isUserPresent = $UP;
$authData->isUserVerified = $UV;
$authData->rpIdHash = $rpIdHash;
$authData->signCount = $signCount;

$restOfBytes = substr($bytes, 37);
$restOfBytesLength = strlen($restOfBytes);
if ($AT) {
assert($restOfBytesLength >= 18);

$aaguid = substr($restOfBytes, 0, 16);
$credentialIdLength = unpack('n', substr($restOfBytes, 16, 2))[1];
assert($restOfBytesLength >= (18 + $credentialIdLength));
$credentialId = substr($restOfBytes, 18, $credentialIdLength);

$rawCredentialPublicKey = substr($restOfBytes, 18 + $credentialIdLength);

$decoder = new Decoder();
$credentialPublicKey = $decoder->decode($rawCredentialPublicKey);

$authData->ACD = [
'aaguid' => $aaguid,
'credentialId' => $credentialId,
'credentialPublicKey' => $credentialPublicKey,
];
// var_dump($decoder->getNumberOfBytesRead());
// cut rest of bytes down based on that ^ ?
}
if ($ED) {
// @codeCoverageIgnoreStart
throw new BadMethodCallException('Not implemented yet');
// @codeCoverageIgnoreEnd
}

return $authData;
}

/** @return ?AttestedCredentialData */
public function getAttestedCredentialData(): ?array
{
return $this->ACD;
}

public function getRpIdHash(): string
{
return $this->rpIdHash;
}

public function getSignCount(): int
{
return $this->signCount;
}

public function isUserPresent(): bool
{
return $this->isUserPresent;
}

/**
* @return array{
* isUserPresent: bool,
* isUserVerified: bool,
* rpIdHash: string,
* signCount: int,
* ACD?: array{
* aaguid: string,
* credentialId: string,
* credentialPublicKey: array{
* kty: int,
* alg: ?int,
* crv: int,
* x: string,
* y: string,
* d: string,
* },
* },
* }
*/
public function __debugInfo(): array
{
$hex = function ($str) {
return '0x' . bin2hex($str);
};
$data = [
'isUserPresent' => $this->isUserPresent,
'isUserVerified' => $this->isUserVerified,
'rpIdHash' => $hex($this->rpIdHash),
'signCount' => $this->signCount,
];

if ($this->ACD) {
// See RFC8152 section 7 (COSE key parameters)
$pk = [
'kty' => $this->ACD['credentialPublicKey'][1], // MUST be 'EC2' (sec 13 tbl 21)
// kid = 2
'alg' => $this->ACD['credentialPublicKey'][3] ?? null,
// key_ops = 4 // must include sign (1)/verify(2) if present, depending on usage
// Base IV = 5

// this would be 'k' if 'kty'===4(Symmetric)
'crv' => $this->ACD['credentialPublicKey'][-1], // (13.1 tbl 22)
'x' => $hex($this->ACD['credentialPublicKey'][-2] ?? ''), // (13.1.1 tbl 23/13.2 tbl 24)
'y' => $hex($this->ACD['credentialPublicKey'][-3] ?? ''), // (13.1.1 tbl 23)
'd' => $hex($this->ACD['credentialPublicKey'][-4] ?? ''), // (13.2 tbl 24)

];
$acd = [
'aaguid' => $hex($this->ACD['aaguid']),
'credentialId' => $hex($this->ACD['credentialId']),
'credentialPublicKey' => $pk,
];
$data['ACD'] = $acd;
}
return $data;
}
}
Loading

0 comments on commit e2b7d9e

Please sign in to comment.