First release
This commit is contained in:
26
src/Attestation/AttestationObject.php
Normal file
26
src/Attestation/AttestationObject.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation;
|
||||
|
||||
use Laragear\WebAuthn\Attestation\Formats\Format;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AttestationObject
|
||||
{
|
||||
/**
|
||||
* Create a new Attestation Object.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\AuthenticatorData $authenticatorData
|
||||
* @param \Laragear\WebAuthn\Attestation\Formats\Format $format
|
||||
* @param string $formatName
|
||||
*/
|
||||
public function __construct(
|
||||
public AuthenticatorData $authenticatorData,
|
||||
public Format $format,
|
||||
public string $formatName)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
530
src/Attestation/AuthenticatorData.php
Normal file
530
src/Attestation/AuthenticatorData.php
Normal file
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation;
|
||||
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\CborDecoder;
|
||||
use Laragear\WebAuthn\Exceptions\DataException;
|
||||
use function base64_encode;
|
||||
use function chr;
|
||||
use function chunk_split;
|
||||
use function intdiv;
|
||||
use function is_array;
|
||||
use function ord;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright © 2021 Lukas Buchs
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This file has been modernized to fit Laravel.
|
||||
*
|
||||
* @author Lukas Buchs
|
||||
* @internal
|
||||
*
|
||||
* DER = Distinguished Encoding Rules;
|
||||
* PEM = Privacy Enhanced Mail, basically BASE64 encoded DER.
|
||||
*/
|
||||
class AuthenticatorData
|
||||
{
|
||||
// COSE encoded keys
|
||||
protected static int $COSE_KTY = 1;
|
||||
protected static int $COSE_ALG = 3;
|
||||
|
||||
// COSE EC2 ES256 P-256 curve
|
||||
protected static int $COSE_CRV = -1;
|
||||
protected static int $COSE_X = -2;
|
||||
protected static int $COSE_Y = -3;
|
||||
|
||||
// COSE RSA PS256
|
||||
protected static int $COSE_N = -1;
|
||||
protected static int $COSE_E = -2;
|
||||
|
||||
protected static int $EC2_TYPE = 2;
|
||||
protected static int $EC2_ES256 = -7;
|
||||
protected static int $EC2_P256 = 1;
|
||||
|
||||
protected static int $RSA_TYPE = 3;
|
||||
protected static int $RSA_RS256 = -257;
|
||||
|
||||
/**
|
||||
* Creates a new Authenticator Data instance from a binary string.
|
||||
*
|
||||
* @param string $relyingPartyIdHash
|
||||
* @param object $flags
|
||||
* @param int $counter
|
||||
* @param object{aaguid: int|bool, credentialId: string, credentialPublicKey: string} $attestedCredentialData
|
||||
* @param array $extensionData
|
||||
*/
|
||||
public function __construct(
|
||||
public string $relyingPartyIdHash,
|
||||
public object $flags,
|
||||
public int $counter,
|
||||
public object $attestedCredentialData,
|
||||
public array $extensionData,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the Relying Party ID hash is the same as the one issued.
|
||||
*
|
||||
* @param string $relyingPartyId
|
||||
* @param bool $hash
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSameRPIdHash(string $relyingPartyId, bool $hash = true): bool
|
||||
{
|
||||
if ($hash) {
|
||||
$relyingPartyId = hash('sha256', $relyingPartyId, true);
|
||||
}
|
||||
|
||||
return hash_equals($relyingPartyId, $this->relyingPartyIdHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Relying Party ID hash is not the same as the one issued.
|
||||
*
|
||||
* @param string $relyingPartyId
|
||||
* @param bool $hash
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNotSameRPIdHash(string $relyingPartyId, bool $hash = true): bool
|
||||
{
|
||||
return ! $this->hasSameRPIdHash($relyingPartyId, $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user was present during the authentication.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasUserPresent(): bool
|
||||
{
|
||||
return $this->flags->userPresent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user was absent during the authentication.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasUserAbsent(): bool
|
||||
{
|
||||
return ! $this->wasUserPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user was actively verified by the authenticator.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasUserVerified(): bool
|
||||
{
|
||||
return $this->flags->userVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user was not actively verified by the authenticator.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasUserNotVerified(): bool
|
||||
{
|
||||
return ! $this->wasUserVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key in PEM format.
|
||||
*
|
||||
* @return string
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
public function getPublicKeyPem(): string
|
||||
{
|
||||
$der = match ($this->attestedCredentialData->credentialPublicKey->kty) {
|
||||
self::$EC2_TYPE => $this->getEc2Der(),
|
||||
self::$RSA_TYPE => $this->getRsaDer(),
|
||||
default => throw new DataException('Invalid credential public key type [kty].'),
|
||||
};
|
||||
|
||||
$pem = '-----BEGIN PUBLIC KEY-----'."\n";
|
||||
$pem .= chunk_split(base64_encode($der), 64, "\n");
|
||||
$pem .= '-----END PUBLIC KEY-----'."\n";
|
||||
|
||||
return $pem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key in U2F format.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPublicKeyU2F(): string
|
||||
{
|
||||
return "\x04". // ECC uncompressed
|
||||
$this->attestedCredentialData->credentialPublicKey->x.
|
||||
$this->attestedCredentialData->credentialPublicKey->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns DER encoded EC2 key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEc2Der(): string
|
||||
{
|
||||
return $this->derSequence(
|
||||
$this->derSequence(
|
||||
$this->derOid("\x2A\x86\x48\xCE\x3D\x02\x01"). // OID 1.2.840.10045.2.1 ecPublicKey
|
||||
$this->derOid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
|
||||
).
|
||||
$this->derBitString($this->getPublicKeyU2F())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns DER encoded RSA key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRsaDer(): string
|
||||
{
|
||||
return $this->derSequence(
|
||||
$this->derSequence(
|
||||
$this->derOid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01"). // OID 1.2.840.113549.1.1.1 rsaEncryption
|
||||
$this->derNullValue()
|
||||
).
|
||||
$this->derBitString(
|
||||
$this->derSequence(
|
||||
$this->derUnsignedInteger($this->attestedCredentialData->credentialPublicKey->n).
|
||||
$this->derUnsignedInteger($this->attestedCredentialData->credentialPublicKey->e)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of a DER encoded string.
|
||||
*
|
||||
* @param int $der
|
||||
* @return string
|
||||
*/
|
||||
protected function derLength(int $der): string
|
||||
{
|
||||
if ($der < 128) {
|
||||
return chr($der);
|
||||
}
|
||||
|
||||
$lenBytes = '';
|
||||
|
||||
while ($der > 0) {
|
||||
$lenBytes = chr($der % 256).$lenBytes;
|
||||
$der = intdiv($der, 256);
|
||||
}
|
||||
|
||||
return chr(0x80 | strlen($lenBytes)).$lenBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string as DER.
|
||||
*
|
||||
* @param string $contents
|
||||
* @return string
|
||||
*/
|
||||
protected function derSequence(string $contents): string
|
||||
{
|
||||
return "\x30".$this->derLength(strlen($contents)).$contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode something an ID of zero as DER.
|
||||
*
|
||||
* @param string $encoded
|
||||
* @return string
|
||||
*/
|
||||
protected function derOid(string $encoded): string
|
||||
{
|
||||
return "\x06".$this->derLength(strlen($encoded)).$encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the bit string as DER.
|
||||
*
|
||||
* @param string $bytes
|
||||
* @return string
|
||||
*/
|
||||
protected function derBitString(string $bytes): string
|
||||
{
|
||||
return "\x03".$this->derLength(strlen($bytes) + 1)."\x00".$bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a null value as DER.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function derNullValue(): string
|
||||
{
|
||||
return "\x05\x00";
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a unsigned integer as DER.
|
||||
*
|
||||
* @param string $bytes
|
||||
* @return string
|
||||
*/
|
||||
protected function derUnsignedInteger(string $bytes): string
|
||||
{
|
||||
$len = strlen($bytes);
|
||||
|
||||
// Remove leading zero bytes
|
||||
for ($i = 0; $i < ($len - 1); $i++) {
|
||||
if (ord($bytes[$i]) !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($i !== 0) {
|
||||
$bytes = substr($bytes, $i);
|
||||
}
|
||||
|
||||
// If most significant bit is set, prefix with another zero to prevent it being seen as negative number
|
||||
if ((ord($bytes[0]) & 0x80) !== 0) {
|
||||
$bytes = "\x00".$bytes;
|
||||
}
|
||||
|
||||
return "\x02".$this->derLength(strlen($bytes)).$bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Authenticator data from a binary string.
|
||||
*
|
||||
* @param string $binary
|
||||
* @return static
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function fromBinary(string $binary): static
|
||||
{
|
||||
if (strlen($binary) < 37) {
|
||||
throw new DataException('Authenticator Data: Invalid input.');
|
||||
}
|
||||
|
||||
$relyingPartyIdHash = substr($binary, 0, 32);
|
||||
|
||||
// flags (1 byte)
|
||||
$flags = static::readFlags(unpack('Cflags', $binary[32])['flags']);
|
||||
|
||||
// signature counter: 32-bit unsigned big-endian integer.
|
||||
$counter = unpack('Nsigncount', substr($binary, 33, 4))['signcount'];
|
||||
|
||||
$offset = 37;
|
||||
|
||||
$attestedCredentialData = $flags->attestedDataIncluded
|
||||
? static::readAttestData($binary, $offset)
|
||||
: (object) null;
|
||||
|
||||
$extensionData = $flags->extensionDataIncluded
|
||||
? static::readExtensionData(substr($binary, $offset))
|
||||
: [];
|
||||
|
||||
return new static($relyingPartyIdHash, $flags, $counter, $attestedCredentialData, $extensionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the flags from flag byte array.
|
||||
*
|
||||
* @param string $binFlag
|
||||
* @return object{userPresent: bool, userVerified: bool, attestedDataIncluded: bool, extensionDataIncluded: bool}
|
||||
*/
|
||||
protected static function readFlags(string $binFlag): object
|
||||
{
|
||||
$flags = (object) [
|
||||
'bit_0' => (bool) ($binFlag & 1),
|
||||
'bit_1' => (bool) ($binFlag & 2),
|
||||
'bit_2' => (bool) ($binFlag & 4),
|
||||
'bit_3' => (bool) ($binFlag & 8),
|
||||
'bit_4' => (bool) ($binFlag & 16),
|
||||
'bit_5' => (bool) ($binFlag & 32),
|
||||
'bit_6' => (bool) ($binFlag & 64),
|
||||
'bit_7' => (bool) ($binFlag & 128),
|
||||
'userPresent' => false,
|
||||
'userVerified' => false,
|
||||
'attestedDataIncluded' => false,
|
||||
'extensionDataIncluded' => false,
|
||||
];
|
||||
|
||||
// named flags
|
||||
$flags->userPresent = $flags->bit_0;
|
||||
$flags->userVerified = $flags->bit_2;
|
||||
$flags->attestedDataIncluded = $flags->bit_6;
|
||||
$flags->extensionDataIncluded = $flags->bit_7;
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the attestation data.
|
||||
*
|
||||
* @param string $binary
|
||||
* @param int $endOffset
|
||||
* @return object{aaguid: int|bool, credentialId: string, credentialPublicKey: string}
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
protected static function readAttestData(string $binary, int &$endOffset): object
|
||||
{
|
||||
if (strlen($binary) <= 55) {
|
||||
throw new DataException('Attested data is missing');
|
||||
}
|
||||
|
||||
// Byte length L of Credential ID, 16-bit unsigned big-endian integer.
|
||||
$length = unpack('nlength', substr($binary, 53, 2))['length'];
|
||||
|
||||
// Set end offset
|
||||
$endOffset = 55 + $length;
|
||||
|
||||
return (object) [
|
||||
'aaguid' => substr($binary, 37, 16),
|
||||
'credentialId' => new ByteBuffer(substr($binary, 55, $length)),
|
||||
'credentialPublicKey' => static::readCredentialPublicKey($binary, 55 + $length, $endOffset)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read COSE key-encoded elliptic curve public key in EC2 format.
|
||||
*
|
||||
* @param string $binary
|
||||
* @param int $offset
|
||||
* @param int $endOffset
|
||||
* @return object
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
protected static function readCredentialPublicKey(string $binary, int $offset, int &$endOffset): object
|
||||
{
|
||||
$enc = CborDecoder::decodePortion($binary, $offset, $endOffset);
|
||||
|
||||
// COSE key-encoded elliptic curve public key in EC2 format
|
||||
$publicKey = (object) [
|
||||
'kty' => $enc[static::$COSE_KTY],
|
||||
'alg' => $enc[static::$COSE_ALG]
|
||||
];
|
||||
|
||||
switch ($publicKey->alg) {
|
||||
case static::$EC2_ES256:
|
||||
static::readCredentialPublicKeyES256($publicKey, $enc);
|
||||
break;
|
||||
case static::$RSA_RS256:
|
||||
static::readCredentialPublicKeyRS256($publicKey, $enc);
|
||||
break;
|
||||
}
|
||||
|
||||
return $publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts ES256 information from COSE encoding.
|
||||
*
|
||||
* @param object $publicKey
|
||||
* @param array $cose
|
||||
* @return object
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
protected static function readCredentialPublicKeyES256(object $publicKey, array $cose): object
|
||||
{
|
||||
$publicKey->crv = $cose[self::$COSE_CRV];
|
||||
$publicKey->x = $cose[self::$COSE_X] instanceof ByteBuffer ? $cose[self::$COSE_X]->getBinaryString() : null;
|
||||
$publicKey->y = $cose[self::$COSE_Y] instanceof ByteBuffer ? $cose[self::$COSE_Y]->getBinaryString() : null;
|
||||
|
||||
if ($publicKey->kty !== self::$EC2_TYPE) {
|
||||
throw new DataException('Public key not in EC2 format');
|
||||
}
|
||||
|
||||
if ($publicKey->alg !== self::$EC2_ES256) {
|
||||
throw new DataException('Signature algorithm not ES256');
|
||||
}
|
||||
|
||||
if ($publicKey->crv !== self::$EC2_P256) {
|
||||
throw new DataException('Curve not P-256');
|
||||
}
|
||||
|
||||
if (strlen($publicKey->x) !== 32) {
|
||||
throw new DataException('Invalid X-coordinate');
|
||||
}
|
||||
|
||||
if (strlen($publicKey->y) !== 32) {
|
||||
throw new DataException('Invalid Y-coordinate');
|
||||
}
|
||||
|
||||
return $publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract RS256 information from COSE.
|
||||
*
|
||||
* @param object $publicKey
|
||||
* @param array $enc
|
||||
* @return void
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
protected static function readCredentialPublicKeyRS256(object $publicKey, array $enc): void
|
||||
{
|
||||
$publicKey->n = $enc[self::$COSE_N] instanceof ByteBuffer ? $enc[self::$COSE_N]->getBinaryString() : null;
|
||||
$publicKey->e = $enc[self::$COSE_E] instanceof ByteBuffer ? $enc[self::$COSE_E]->getBinaryString() : null;
|
||||
|
||||
if ($publicKey->kty !== self::$RSA_TYPE) {
|
||||
throw new DataException('Public key not in RSA format');
|
||||
}
|
||||
|
||||
if ($publicKey->alg !== self::$RSA_RS256) {
|
||||
throw new DataException('Signature algorithm not ES256');
|
||||
}
|
||||
|
||||
if (strlen($publicKey->n) !== 256) {
|
||||
throw new DataException('Invalid RSA modulus');
|
||||
}
|
||||
|
||||
if (strlen($publicKey->e) !== 3) {
|
||||
throw new DataException('Invalid RSA public exponent');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads CBOR encoded extension data.
|
||||
*
|
||||
* @param string $binary
|
||||
* @return array<int, string>
|
||||
* @throws \Laragear\WebAuthn\Exceptions\DataException
|
||||
*/
|
||||
protected static function readExtensionData(string $binary): array
|
||||
{
|
||||
$ext = CborDecoder::decode($binary);
|
||||
|
||||
return is_array($ext) ? $ext : throw new DataException('Invalid extension data');
|
||||
}
|
||||
}
|
||||
40
src/Attestation/Creator/AttestationCreation.php
Normal file
40
src/Attestation/Creator/AttestationCreation.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||
use Laragear\WebAuthn\JsonTransport;
|
||||
|
||||
class AttestationCreation
|
||||
{
|
||||
|
||||
public const ATTACHMENT_CROSS_PLATFORM = 'cross-platform';
|
||||
public const ATTACHMENT_PLATFORM = 'platform';
|
||||
|
||||
/**
|
||||
* The underlying JSON representation of the Assertion Challenge.
|
||||
*
|
||||
* @var \Laragear\WebAuthn\JsonTransport
|
||||
*/
|
||||
public JsonTransport $json;
|
||||
|
||||
/**
|
||||
* Create a new Attestation Instructions instance.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable $user
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string|null $residentKey
|
||||
* @param string|null $userVerification
|
||||
* @param bool $uniqueCredentials
|
||||
*/
|
||||
public function __construct(
|
||||
public WebAuthnAuthenticatable $user,
|
||||
public Request $request,
|
||||
public ?string $residentKey = null,
|
||||
public ?string $userVerification = null,
|
||||
public bool $uniqueCredentials = true,
|
||||
) {
|
||||
$this->json = new JsonTransport();
|
||||
}
|
||||
}
|
||||
28
src/Attestation/Creator/AttestationCreator.php
Normal file
28
src/Attestation/Creator/AttestationCreator.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator;
|
||||
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @method \Laragear\WebAuthn\Assertion\Creator\AssertionCreation thenReturn()
|
||||
*/
|
||||
class AttestationCreator extends Pipeline
|
||||
{
|
||||
/**
|
||||
* The array of class pipes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pipes = [
|
||||
Pipes\AddRelyingParty::class,
|
||||
Pipes\SetResidentKeyConfiguration::class,
|
||||
Pipes\MayRequireUserVerification::class,
|
||||
Pipes\AddUserDescriptor::class,
|
||||
Pipes\AddAcceptedAlgorithms::class,
|
||||
Pipes\MayPreventDuplicateCredentials::class,
|
||||
Pipes\CreateAttestationChallenge::class,
|
||||
];
|
||||
}
|
||||
34
src/Attestation/Creator/Pipes/AddAcceptedAlgorithms.php
Normal file
34
src/Attestation/Creator/Pipes/AddAcceptedAlgorithms.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AddAcceptedAlgorithms
|
||||
{
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
$attestable->json->set('pubKeyCredParams', [
|
||||
['type' => 'public-key', 'alg' => -7],
|
||||
['type' => 'public-key', 'alg' => -257],
|
||||
]);
|
||||
|
||||
// Currently we don't support direct attestation. In other words, it won't ask
|
||||
// for attestation data from the authenticator to cross-check later against
|
||||
// root certificates. We may add this in the future, but not guaranteed.
|
||||
$attestable->json->set('attestation', 'none');
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
41
src/Attestation/Creator/Pipes/AddRelyingParty.php
Normal file
41
src/Attestation/Creator/Pipes/AddRelyingParty.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Config\Repository;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AddRelyingParty
|
||||
{
|
||||
/**
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param \Illuminate\Config\Repository $config
|
||||
*/
|
||||
public function __construct(protected Repository $config)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
$attestable->json->set('rp.name', $this->config->get('webauthn.relying_party.name'));
|
||||
|
||||
if ($id = $this->config->get('webauthn.relying_party.id')) {
|
||||
$attestable->json->set('rp.id', $id);
|
||||
}
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
33
src/Attestation/Creator/Pipes/AddUserDescriptor.php
Normal file
33
src/Attestation/Creator/Pipes/AddUserDescriptor.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AddUserDescriptor
|
||||
{
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
$config = $attestable->user->webAuthnData();
|
||||
|
||||
// Create a new User UUID if it doesn't existe already in the credentials.
|
||||
$config['id'] = $attestable->user->webAuthnCredentials()->value('user_id')
|
||||
?: Str::uuid()->getHex()->toString();
|
||||
|
||||
$attestable->json->set('user', $config);
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
49
src/Attestation/Creator/Pipes/CreateAttestationChallenge.php
Normal file
49
src/Attestation/Creator/Pipes/CreateAttestationChallenge.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Contracts\Cache\Factory;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
use Laragear\WebAuthn\Attestation\SessionChallenge;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CreateAttestationChallenge
|
||||
{
|
||||
use SessionChallenge;
|
||||
|
||||
/**
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param \Illuminate\Config\Repository $config
|
||||
* @param \Illuminate\Contracts\Cache\Factory $cache
|
||||
*/
|
||||
public function __construct(protected Repository $config, protected Factory $cache)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
$attestable->json->set('timeout', $this->config->get('webauthn.challenge.timeout') * 1000);
|
||||
|
||||
$challenge = $this->storeChallenge($attestable->request, $attestable->userVerification, [
|
||||
'user_uuid' => $attestable->json->get('user.id'),
|
||||
'user_handle' => $attestable->json->get('user.name'),
|
||||
]);
|
||||
|
||||
$attestable->json->set('challenge', $challenge->data);
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MayPreventDuplicateCredentials
|
||||
{
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
if ($attestable->uniqueCredentials) {
|
||||
$attestable->json->set('excludeCredentials', $this->credentials($attestable->user));
|
||||
}
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of credentials ready to be inserted into the Attestable JSON.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable $user
|
||||
* @return array
|
||||
*/
|
||||
protected function credentials(WebAuthnAuthenticatable $user): array
|
||||
{
|
||||
return $user
|
||||
->webAuthnCredentials()
|
||||
->get(['id', 'transports'])
|
||||
->map(static function (WebAuthnCredential $credential): array {
|
||||
return array_filter([
|
||||
'id'=> $credential->getKey(),
|
||||
'type' => 'public-key',
|
||||
'transports' => $credential->transports
|
||||
]);
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
28
src/Attestation/Creator/Pipes/MayRequireUserVerification.php
Normal file
28
src/Attestation/Creator/Pipes/MayRequireUserVerification.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MayRequireUserVerification
|
||||
{
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
if ($attestable->userVerification) {
|
||||
$attestable->json->set('authenticatorSelection.userVerification', $attestable->userVerification);
|
||||
}
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Creator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Creator\AttestationCreation;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SetResidentKeyConfiguration
|
||||
{
|
||||
/**
|
||||
* Handle the Attestation creation
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Creator\AttestationCreation $attestable
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(AttestationCreation $attestable, Closure $next): mixed
|
||||
{
|
||||
if ($attestable->residentKey) {
|
||||
$attestable->json->set('authenticatorSelection.residentKey', $attestable->residentKey);
|
||||
|
||||
$verifiesUser = $attestable->residentKey === WebAuthn::RESIDENT_KEY_REQUIRED;
|
||||
|
||||
$attestable->json->set('authenticatorSelection.requireResidentKey', $verifiesUser);
|
||||
|
||||
if ($verifiesUser) {
|
||||
$attestable->userVerification = WebAuthn::USER_VERIFICATION_REQUIRED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $next($attestable);
|
||||
}
|
||||
}
|
||||
53
src/Attestation/Formats/Format.php
Normal file
53
src/Attestation/Formats/Format.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Formats;
|
||||
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright © 2021 Lukas Buchs
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This is a base class that hold common tasks for different Attestation Statements formats.
|
||||
*
|
||||
* This file has been modernized to fit Laravel.
|
||||
*
|
||||
* @author Lukas Buchs
|
||||
* @see https://www.iana.org/assignments/webauthn/webauthn.xhtml
|
||||
* @internal
|
||||
*/
|
||||
abstract class Format
|
||||
{
|
||||
/**
|
||||
* Create a new Attestation Format.
|
||||
*
|
||||
* @param array{fmt: string, attStmt: array, authData: \Laragear\WebAuthn\ByteBuffer} $attestationObject
|
||||
* @param \Laragear\WebAuthn\Attestation\AuthenticatorData $authenticatorData
|
||||
*/
|
||||
public function __construct(public array $attestationObject, public AuthenticatorData $authenticatorData)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
12
src/Attestation/Formats/None.php
Normal file
12
src/Attestation/Formats/None.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Formats;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class None extends Format
|
||||
{
|
||||
|
||||
}
|
||||
44
src/Attestation/SessionChallenge.php
Normal file
44
src/Attestation/SessionChallenge.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
|
||||
trait SessionChallenge
|
||||
{
|
||||
/**
|
||||
* Stores an Attestation challenge into the Cache.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string|null $verify
|
||||
* @param array $options
|
||||
* @return \Laragear\WebAuthn\Challenge
|
||||
*/
|
||||
protected function storeChallenge(Request $request, ?string $verify, array $options = []): Challenge
|
||||
{
|
||||
$challenge = $this->createChallenge($verify, $options);
|
||||
|
||||
$request->session()->put($this->config->get('webauthn.challenge.key'), $challenge);
|
||||
|
||||
return $challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Challenge using the default timeout.
|
||||
*
|
||||
* @param string|null $verify
|
||||
* @param array $options
|
||||
* @return \Laragear\WebAuthn\Challenge
|
||||
*/
|
||||
protected function createChallenge(?string $verify, array $options = []): Challenge
|
||||
{
|
||||
return Challenge::random(
|
||||
$this->config->get('webauthn.challenge.bytes'),
|
||||
$this->config->get('webauthn.challenge.timeout'),
|
||||
$verify === WebAuthn::USER_VERIFICATION_REQUIRED,
|
||||
$options,
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/Attestation/Validator/AttestationValidation.php
Normal file
35
src/Attestation/Validator/AttestationValidation.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Laragear\WebAuthn\Attestation\AttestationObject;
|
||||
use Laragear\WebAuthn\Challenge;
|
||||
use Laragear\WebAuthn\ClientDataJson;
|
||||
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
|
||||
class AttestationValidation
|
||||
{
|
||||
/**
|
||||
* Create a new Attestation Validation procedure
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable $user
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Laragear\WebAuthn\Challenge|null $challenge
|
||||
* @param \Laragear\WebAuthn\Attestation\AttestationObject|null $attestationObject
|
||||
* @param \Laragear\WebAuthn\ClientDataJson|null $clientDataJson
|
||||
* @param \Laragear\WebAuthn\Models\WebAuthnCredential|null $credential
|
||||
*/
|
||||
public function __construct(
|
||||
public WebAuthnAuthenticatable $user,
|
||||
public Request $request,
|
||||
public ?Challenge $challenge = null,
|
||||
public ?AttestationObject $attestationObject = null,
|
||||
public ?ClientDataJson $clientDataJson = null,
|
||||
public ?WebAuthnCredential $credential = null,
|
||||
)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
32
src/Attestation/Validator/AttestationValidator.php
Normal file
32
src/Attestation/Validator/AttestationValidator.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator;
|
||||
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @method \Laragear\WebAuthn\Attestation\Validator\AttestationValidation thenReturn()
|
||||
*/
|
||||
class AttestationValidator extends Pipeline
|
||||
{
|
||||
/**
|
||||
* The array of class pipes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pipes = [
|
||||
Pipes\RetrieveChallenge::class,
|
||||
Pipes\CompileClientDataJson::class,
|
||||
Pipes\CompileAttestationObject::class,
|
||||
Pipes\AttestationIsForCreation::class,
|
||||
Pipes\CheckChallengeSame::class,
|
||||
Pipes\CheckOriginSecure::class,
|
||||
Pipes\CheckRelyingPartyIdContained::class,
|
||||
Pipes\CheckRelyingPartyHashSame::class,
|
||||
Pipes\CheckUserInteraction::class,
|
||||
Pipes\CredentialIdShouldNotBeDuplicated::class,
|
||||
Pipes\MakeWebAuthnCredential::class,
|
||||
];
|
||||
}
|
||||
34
src/Attestation/Validator/Pipes/AttestationIsForCreation.php
Normal file
34
src/Attestation/Validator/Pipes/AttestationIsForCreation.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AttestationException;
|
||||
|
||||
/**
|
||||
* 7. Verify that the value of C.type is webauthn.create.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AttestationIsForCreation
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Attestation Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AttestationException
|
||||
*/
|
||||
public function handle(AttestationValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($validation->clientDataJson->type !== 'webauthn.create') {
|
||||
throw AttestationException::make('Response is not for creating WebAuthn Credentials.');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
}
|
||||
17
src/Attestation/Validator/Pipes/CheckChallengeSame.php
Normal file
17
src/Attestation/Validator/Pipes/CheckChallengeSame.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckChallengeSame as BaseCheckChallengeSame;
|
||||
|
||||
/**
|
||||
* 8. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckChallengeSame extends BaseCheckChallengeSame
|
||||
{
|
||||
//
|
||||
}
|
||||
10
src/Attestation/Validator/Pipes/CheckOriginSecure.php
Normal file
10
src/Attestation/Validator/Pipes/CheckOriginSecure.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckOriginSecure as BaseCheckOriginSame;
|
||||
|
||||
class CheckOriginSecure extends BaseCheckOriginSame
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\SharedPipes\CheckRelyingPartyHashSame as BaseCheckRelyingPartyHashSame;
|
||||
|
||||
/**
|
||||
* 13. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckRelyingPartyHashSame extends BaseCheckRelyingPartyHashSame
|
||||
{
|
||||
/**
|
||||
* Return the Attestation data to check the RP ID Hash.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation|\Laragear\WebAuthn\Assertion\Validator\AssertionValidation $validation
|
||||
* @return \Laragear\WebAuthn\Attestation\AuthenticatorData
|
||||
*/
|
||||
protected function authenticatorData(AssertionValidation|AttestationValidation $validation): AuthenticatorData
|
||||
{
|
||||
return $validation->attestationObject->authenticatorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Relying Party ID from the config or credential.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Assertion\Validator\AssertionValidation|\Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @return string
|
||||
*/
|
||||
protected function relyingPartyId(AssertionValidation|AttestationValidation $validation): string
|
||||
{
|
||||
return $this->config->get('webauthn.relaying_party.id') ?? $this->config->get('app.url');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckRelyingPartyIdContained as BaseCheckRelyingPartyIdSame;
|
||||
|
||||
/**
|
||||
* 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckRelyingPartyIdContained extends BaseCheckRelyingPartyIdSame
|
||||
{
|
||||
//
|
||||
}
|
||||
17
src/Attestation/Validator/Pipes/CheckUserInteraction.php
Normal file
17
src/Attestation/Validator/Pipes/CheckUserInteraction.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CheckUserInteraction as BaseCheckUserInteraction;
|
||||
|
||||
/**
|
||||
* 14. Verify that the User Present bit of the flags in authData is set.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CheckUserInteraction extends BaseCheckUserInteraction
|
||||
{
|
||||
//
|
||||
}
|
||||
100
src/Attestation/Validator/Pipes/CompileAttestationObject.php
Normal file
100
src/Attestation/Validator/Pipes/CompileAttestationObject.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use Laragear\WebAuthn\Attestation\AttestationObject;
|
||||
use Laragear\WebAuthn\Attestation\AuthenticatorData;
|
||||
use Laragear\WebAuthn\Attestation\Formats\None;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\ByteBuffer;
|
||||
use Laragear\WebAuthn\CborDecoder;
|
||||
use Laragear\WebAuthn\Exceptions\AttestationException;
|
||||
use Laragear\WebAuthn\Exceptions\DataException;
|
||||
use function base64_decode;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* 12. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||
* structure to obtain the attestation statement format fmt, the authenticator data authData,
|
||||
* and the attestation statement attStmt.
|
||||
*
|
||||
* 18. Determine the attestation statement format by performing a USASCII case-sensitive match on
|
||||
* fmt against the set of supported WebAuthn Attestation Statement Format Identifier values.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CompileAttestationObject
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Attestation Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AttestationException
|
||||
*/
|
||||
public function handle(AttestationValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$data = $this->decodeCborBase64($validation->request);
|
||||
|
||||
// Here we would receive the attestation formats and decode them. Since we're
|
||||
// only support the universal "none" we can just check if it's equal or not.
|
||||
// Later we may support multiple authenticator formats through a PHP match.
|
||||
if ($data['fmt'] !== 'none') {
|
||||
throw AttestationException::make("Format name [{$data['fmt']}] is invalid.");
|
||||
}
|
||||
|
||||
try {
|
||||
$authenticatorData = AuthenticatorData::fromBinary($data['authData']->getBinaryString());
|
||||
} catch (DataException $e) {
|
||||
throw AttestationException::make($e->getMessage());
|
||||
}
|
||||
|
||||
$validation->attestationObject = new AttestationObject(
|
||||
$authenticatorData, new None($data, $authenticatorData), $data['fmt']
|
||||
);
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array map from a BASE64 encoded CBOR string.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AttestationException
|
||||
*/
|
||||
#[ArrayShape(["fmt" => "string", "attStmt" => "array", "authData" => ByteBuffer::class])]
|
||||
protected function decodeCborBase64(Request $request): array
|
||||
{
|
||||
try {
|
||||
$data = CborDecoder::decode(base64_decode($request->json('response.attestationObject', '')));
|
||||
} catch (DataException $e) {
|
||||
throw AttestationException::make($e->getMessage());
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw AttestationException::make('CBOR Object is anything but an array.');
|
||||
}
|
||||
|
||||
if (!isset($data['fmt']) || !is_string($data['fmt'])) {
|
||||
throw AttestationException::make('Format is missing or invalid.');
|
||||
}
|
||||
|
||||
if (!isset($data['attStmt']) || !is_array($data['attStmt'])) {
|
||||
throw AttestationException::make('Statement is missing or invalid.');
|
||||
}
|
||||
|
||||
if (!isset($data['authData']) || !$data['authData'] instanceof ByteBuffer) {
|
||||
throw AttestationException::make('Authenticator Data is missing or invalid.');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
20
src/Attestation/Validator/Pipes/CompileClientDataJson.php
Normal file
20
src/Attestation/Validator/Pipes/CompileClientDataJson.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\CompileClientDataJson as BaseCompileClientDataJson;
|
||||
|
||||
/**
|
||||
* 5. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON.
|
||||
*
|
||||
* 6. Let C, the client data claimed as collected during the credential creation, be the result of
|
||||
* running an implementation-specific JSON parser on JSONtext.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CompileClientDataJson extends BaseCompileClientDataJson
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AttestationException;
|
||||
use Laragear\WebAuthn\Models\WebAuthnCredential;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CredentialIdShouldNotBeDuplicated
|
||||
{
|
||||
/**
|
||||
* Handle the incoming Attestation Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AttestationException
|
||||
*/
|
||||
public function handle(AttestationValidation $validation, Closure $next): mixed
|
||||
{
|
||||
if ($this->credentialAlreadyExists($validation)) {
|
||||
throw AttestationException::make('Credential ID already exists in the database.');
|
||||
}
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a WebAuthn Credential by the issued ID.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @return bool
|
||||
*/
|
||||
protected function credentialAlreadyExists(AttestationValidation $validation): bool
|
||||
{
|
||||
return WebAuthnCredential::whereKey($validation->request->json('id'))->exists();
|
||||
}
|
||||
}
|
||||
70
src/Attestation/Validator/Pipes/MakeWebAuthnCredential.php
Normal file
70
src/Attestation/Validator/Pipes/MakeWebAuthnCredential.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Laragear\WebAuthn\Attestation\Validator\AttestationValidation;
|
||||
use Laragear\WebAuthn\Exceptions\AttestationException;
|
||||
use Laragear\WebAuthn\Exceptions\DataException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MakeWebAuthnCredential
|
||||
{
|
||||
/**
|
||||
* Create a new pipe instance.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Config\Repository $config
|
||||
*/
|
||||
public function __construct(protected Repository $config)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the incoming Attestation Validation.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
* @throws \Laragear\WebAuthn\Exceptions\AttestationException
|
||||
*/
|
||||
public function handle(AttestationValidation $validation, Closure $next): mixed
|
||||
{
|
||||
$validation->credential = $validation->user->makeWebAuthnCredential([
|
||||
'id' => $validation->request->json('id'),
|
||||
|
||||
'user_id' => $validation->challenge->properties['user_uuid'],
|
||||
'alias' => $validation->request->json('response.alias'),
|
||||
|
||||
'counter' => $validation->attestationObject->authenticatorData->counter,
|
||||
'rp_id' => $this->config->get('webauthn.relaying_party.id') ?? $this->config->get('app.url'),
|
||||
'origin' => $validation->clientDataJson->origin,
|
||||
'transports' => $validation->request->json('response.transports'),
|
||||
'aaguid' => Uuid::fromBytes($validation->attestationObject->authenticatorData->attestedCredentialData->aaguid),
|
||||
|
||||
'public_key' => $this->getPublicKeyAsPem($validation),
|
||||
'attestation_format' => $validation->attestationObject->formatName,
|
||||
]);
|
||||
|
||||
return $next($validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a public key from the credentials as a PEM string.
|
||||
*
|
||||
* @param \Laragear\WebAuthn\Attestation\Validator\AttestationValidation $validation
|
||||
* @return string
|
||||
*/
|
||||
protected function getPublicKeyAsPem(AttestationValidation $validation): string
|
||||
{
|
||||
try {
|
||||
return $validation->attestationObject->authenticatorData->getPublicKeyPem();
|
||||
} catch (DataException $e) {
|
||||
throw AttestationException::make($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Attestation/Validator/Pipes/RetrieveChallenge.php
Normal file
13
src/Attestation/Validator/Pipes/RetrieveChallenge.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Laragear\WebAuthn\Attestation\Validator\Pipes;
|
||||
|
||||
use Laragear\WebAuthn\SharedPipes\RetrieveChallenge as BaseRetrieveChallenge;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RetrieveChallenge extends BaseRetrieveChallenge
|
||||
{
|
||||
//
|
||||
}
|
||||
Reference in New Issue
Block a user