diff --git a/data/web/inc/lib/WebAuthn_/Attestation/AttestationObject.php b/data/web/inc/lib/WebAuthn_/Attestation/AttestationObject.php deleted file mode 100644 index f44a8151..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/AttestationObject.php +++ /dev/null @@ -1,153 +0,0 @@ -_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString()); - - // Format ok? - if (!in_array($enc['fmt'], $allowedFormats)) { - throw new WebAuthnException('invalid atttestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA); - } - - switch ($enc['fmt']) { - case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break; - case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break; - case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break; - case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break; - case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break; - case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break; - default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA); - } - } - - /** - * returns the attestation public key in PEM format - * @return AuthenticatorData - */ - public function getAuthenticatorData() { - return $this->_authenticatorData; - } - - /** - * returns the certificate chain as PEM - * @return string|null - */ - public function getCertificateChain() { - return $this->_attestationFormat->getCertificateChain(); - } - - /** - * return the certificate issuer as string - * @return string - */ - public function getCertificateIssuer() { - $pem = $this->getCertificatePem(); - $issuer = ''; - if ($pem) { - $certInfo = \openssl_x509_parse($pem); - if (\is_array($certInfo) && \is_array($certInfo['issuer'])) { - if ($certInfo['issuer']['CN']) { - $issuer .= \trim($certInfo['issuer']['CN']); - } - if ($certInfo['issuer']['O'] || $certInfo['issuer']['OU']) { - if ($issuer) { - $issuer .= ' (' . \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']) . ')'; - } else { - $issuer .= \trim($certInfo['issuer']['O'] . ' ' . $certInfo['issuer']['OU']); - } - } - } - } - - return $issuer; - } - - /** - * return the certificate subject as string - * @return string - */ - public function getCertificateSubject() { - $pem = $this->getCertificatePem(); - $subject = ''; - if ($pem) { - $certInfo = \openssl_x509_parse($pem); - if (\is_array($certInfo) && \is_array($certInfo['subject'])) { - if ($certInfo['subject']['CN']) { - $subject .= \trim($certInfo['subject']['CN']); - } - if ($certInfo['subject']['O'] || $certInfo['subject']['OU']) { - if ($subject) { - $subject .= ' (' . \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']) . ')'; - } else { - $subject .= \trim($certInfo['subject']['O'] . ' ' . $certInfo['subject']['OU']); - } - } - } - } - - return $subject; - } - - /** - * returns the key certificate in PEM format - * @return string - */ - public function getCertificatePem() { - return $this->_attestationFormat->getCertificatePem(); - } - - /** - * checks validity of the signature - * @param string $clientDataHash - * @return bool - * @throws WebAuthnException - */ - public function validateAttestation($clientDataHash) { - return $this->_attestationFormat->validateAttestation($clientDataHash); - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - return $this->_attestationFormat->validateRootCertificate($rootCas); - } - - /** - * checks if the RpId-Hash is valid - * @param string$rpIdHash - * @return bool - */ - public function validateRpIdHash($rpIdHash) { - return $rpIdHash === $this->_authenticatorData->getRpIdHash(); - } -} diff --git a/data/web/inc/lib/WebAuthn_/Attestation/AuthenticatorData.php b/data/web/inc/lib/WebAuthn_/Attestation/AuthenticatorData.php deleted file mode 100644 index 374d9ab4..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/AuthenticatorData.php +++ /dev/null @@ -1,423 +0,0 @@ -_binary = $binary; - - // Read infos from binary - // https://www.w3.org/TR/webauthn/#sec-authenticator-data - - // RP ID - $this->_rpIdHash = \substr($binary, 0, 32); - - // flags (1 byte) - $flags = \unpack('Cflags', \substr($binary, 32, 1))['flags']; - $this->_flags = $this->_readFlags($flags); - - // signature counter: 32-bit unsigned big-endian integer. - $this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount']; - - $offset = 37; - // https://www.w3.org/TR/webauthn/#sec-attested-credential-data - if ($this->_flags->attestedDataIncluded) { - $this->_attestedCredentialData = $this->_readAttestData($binary, $offset); - } - - if ($this->_flags->extensionDataIncluded) { - $this->_readExtensionData(\substr($binary, $offset)); - } - } - - /** - * Authenticator Attestation Globally Unique Identifier, a unique number - * that identifies the model of the authenticator (not the specific instance - * of the authenticator) - * The aaguid may be 0 if the user is using a old u2f device and/or if - * the browser is using the fido-u2f format. - * @return string - * @throws WebAuthnException - */ - public function getAAGUID() { - if (!($this->_attestedCredentialData instanceof \stdClass)) { - throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); - } - return $this->_attestedCredentialData->aaguid; - } - - /** - * returns the authenticatorData as binary - * @return string - */ - public function getBinary() { - return $this->_binary; - } - - /** - * returns the credentialId - * @return string - * @throws WebAuthnException - */ - public function getCredentialId() { - if (!($this->_attestedCredentialData instanceof \stdClass)) { - throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA); - } - return $this->_attestedCredentialData->credentialId; - } - - /** - * returns the public key in PEM format - * @return string - */ - public function getPublicKeyPem() { - $der = null; - switch ($this->_attestedCredentialData->credentialPublicKey->kty) { - case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break; - case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break; - default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA); - } - - $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 - * @throws WebAuthnException - */ - public function getPublicKeyU2F() { - if (!($this->_attestedCredentialData instanceof \stdClass)) { - throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); - } - return "\x04" . // ECC uncompressed - $this->_attestedCredentialData->credentialPublicKey->x . - $this->_attestedCredentialData->credentialPublicKey->y; - } - - /** - * returns the SHA256 hash of the relying party id (=hostname) - * @return string - */ - public function getRpIdHash() { - return $this->_rpIdHash; - } - - /** - * returns the sign counter - * @return int - */ - public function getSignCount() { - return $this->_signCount; - } - - /** - * returns true if the user is present - * @return boolean - */ - public function getUserPresent() { - return $this->_flags->userPresent; - } - - /** - * returns true if the user is verified - * @return boolean - */ - public function getUserVerified() { - return $this->_flags->userVerified; - } - - // ----------------------------------------------- - // PRIVATE - // ----------------------------------------------- - - /** - * Returns DER encoded EC2 key - * @return string - */ - private function _getEc2Der() { - return $this->_der_sequence( - $this->_der_sequence( - $this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey - $this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1 - ) . - $this->_der_bitString($this->getPublicKeyU2F()) - ); - } - - /** - * Returns DER encoded RSA key - * @return string - */ - private function _getRsaDer() { - return $this->_der_sequence( - $this->_der_sequence( - $this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption - $this->_der_nullValue() - ) . - $this->_der_bitString( - $this->_der_sequence( - $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) . - $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e) - ) - ) - ); - } - - /** - * reads the flags from flag byte - * @param string $binFlag - * @return \stdClass - */ - private function _readFlags($binFlag) { - $flags = new \stdClass(); - - $flags->bit_0 = !!($binFlag & 1); - $flags->bit_1 = !!($binFlag & 2); - $flags->bit_2 = !!($binFlag & 4); - $flags->bit_3 = !!($binFlag & 8); - $flags->bit_4 = !!($binFlag & 16); - $flags->bit_5 = !!($binFlag & 32); - $flags->bit_6 = !!($binFlag & 64); - $flags->bit_7 = !!($binFlag & 128); - - // named flags - $flags->userPresent = $flags->bit_0; - $flags->userVerified = $flags->bit_2; - $flags->attestedDataIncluded = $flags->bit_6; - $flags->extensionDataIncluded = $flags->bit_7; - return $flags; - } - - /** - * read attested data - * @param string $binary - * @param int $endOffset - * @return \stdClass - * @throws WebAuthnException - */ - private function _readAttestData($binary, &$endOffset) { - $attestedCData = new \stdClass(); - if (\strlen($binary) <= 55) { - throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA); - } - - // The AAGUID of the authenticator - $attestedCData->aaguid = \substr($binary, 37, 16); - - //Byte length L of Credential ID, 16-bit unsigned big-endian integer. - $length = \unpack('nlength', \substr($binary, 53, 2))['length']; - $attestedCData->credentialId = \substr($binary, 55, $length); - - // set end offset - $endOffset = 55 + $length; - - // extract public key - $attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset); - - return $attestedCData; - } - - /** - * reads COSE key-encoded elliptic curve public key in EC2 format - * @param string $binary - * @param int $endOffset - * @return \stdClass - * @throws WebAuthnException - */ - private function _readCredentialPublicKey($binary, $offset, &$endOffset) { - $enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset); - - // COSE key-encoded elliptic curve public key in EC2 format - $credPKey = new \stdClass(); - $credPKey->kty = $enc[self::$_COSE_KTY]; - $credPKey->alg = $enc[self::$_COSE_ALG]; - - switch ($credPKey->alg) { - case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break; - case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break; - } - - return $credPKey; - } - - /** - * extract ES256 informations from cose - * @param \stdClass $credPKey - * @param \stdClass $enc - * @throws WebAuthnException - */ - private function _readCredentialPublicKeyES256(&$credPKey, $enc) { - $credPKey->crv = $enc[self::$_COSE_CRV]; - $credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null; - $credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null; - unset ($enc); - - // Validation - if ($credPKey->kty !== self::$_EC2_TYPE) { - throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if ($credPKey->alg !== self::$_EC2_ES256) { - throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if ($credPKey->crv !== self::$_EC2_P256) { - throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if (\strlen($credPKey->x) !== 32) { - throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if (\strlen($credPKey->y) !== 32) { - throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); - } - } - - /** - * extract RS256 informations from COSE - * @param \stdClass $credPKey - * @param \stdClass $enc - * @throws WebAuthnException - */ - private function _readCredentialPublicKeyRS256(&$credPKey, $enc) { - $credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null; - $credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null; - unset ($enc); - - // Validation - if ($credPKey->kty !== self::$_RSA_TYPE) { - throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if ($credPKey->alg !== self::$_RSA_RS256) { - throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if (\strlen($credPKey->n) !== 256) { - throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if (\strlen($credPKey->e) !== 3) { - throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY); - } - - } - - /** - * reads cbor encoded extension data. - * @param string $binary - * @return array - * @throws WebAuthnException - */ - private function _readExtensionData($binary) { - $ext = CborDecoder::decode($binary); - if (!\is_array($ext)) { - throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA); - } - - return $ext; - } - - - // --------------- - // DER functions - // --------------- - - private function _der_length($len) { - if ($len < 128) { - return \chr($len); - } - $lenBytes = ''; - while ($len > 0) { - $lenBytes = \chr($len % 256) . $lenBytes; - $len = \intdiv($len, 256); - } - return \chr(0x80 | \strlen($lenBytes)) . $lenBytes; - } - - private function _der_sequence($contents) { - return "\x30" . $this->_der_length(\strlen($contents)) . $contents; - } - - private function _der_oid($encoded) { - return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded; - } - - private function _der_bitString($bytes) { - return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes; - } - - private function _der_nullValue() { - return "\x05\x00"; - } - - private function _der_unsignedInteger($bytes) { - $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->_der_length(\strlen($bytes)) . $bytes; - } -} diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidKey.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidKey.php deleted file mode 100644 index aa6f1abb..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidKey.php +++ /dev/null @@ -1,95 +0,0 @@ -_attestationObject['attStmt']; - - if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { - throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { - throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - $this->_alg = $attStmt['alg']; - $this->_signature = $attStmt['sig']->getBinaryString(); - $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); - - if (count($attStmt['x5c']) > 1) { - for ($i=1; $i_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString(); - } - unset ($i); - } - } - - - /* - * returns the key certificate in PEM format - * @return string - */ - public function getCertificatePem() { - return $this->_createCertificatePem($this->_x5c); - } - - /** - * @param string $clientDataHash - */ - public function validateAttestation($clientDataHash) { - $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); - - if ($publicKey === false) { - throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); - } - - // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using the attestation public key in attestnCert with the algorithm specified in alg. - $dataToVerify = $this->_authenticatorData->getBinary(); - $dataToVerify .= $clientDataHash; - - $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); - - // check certificate - return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - $chainC = $this->_createX5cChainFile(); - if ($chainC) { - $rootCas[] = $chainC; - } - - $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); - if ($v === -1) { - throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - return $v; - } -} - diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidSafetyNet.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidSafetyNet.php deleted file mode 100644 index 70f4212a..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/AndroidSafetyNet.php +++ /dev/null @@ -1,140 +0,0 @@ -_attestationObject['attStmt']; - - if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) { - throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) { - throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); - } - - $response = $attStmt['response']->getBinaryString(); - - // Response is a JWS [RFC7515] object in Compact Serialization. - // JWSs have three segments separated by two period ('.') characters - $parts = \explode('.', $response); - unset ($response); - if (\count($parts) !== 3) { - throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA); - } - - $header = $this->_base64url_decode($parts[0]); - $payload = $this->_base64url_decode($parts[1]); - $this->_signature = $this->_base64url_decode($parts[2]); - $this->_signedValue = $parts[0] . '.' . $parts[1]; - unset ($parts); - - $header = \json_decode($header); - $payload = \json_decode($payload); - - if (!($header instanceof \stdClass)) { - throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA); - } - if (!($payload instanceof \stdClass)) { - throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA); - } - - if (!$header->x5c || !is_array($header->x5c) || count($header->x5c) === 0) { - throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA); - } - - // algorithm - if (!\in_array($header->alg, array('RS256', 'ES256'))) { - throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA); - } - - $this->_x5c = \base64_decode($header->x5c[0]); - $this->_payload = $payload; - - if (count($header->x5c) > 1) { - for ($i=1; $ix5c); $i++) { - $this->_x5c_chain[] = \base64_decode($header->x5c[$i]); - } - unset ($i); - } - } - - - /* - * returns the key certificate in PEM format - * @return string - */ - public function getCertificatePem() { - return $this->_createCertificatePem($this->_x5c); - } - - /** - * @param string $clientDataHash - */ - public function validateAttestation($clientDataHash) { - $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); - - // Verify that the nonce in the response is identical to the Base64 encoding - // of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash. - if (!$this->_payload->nonce || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) { - throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA); - } - - // Verify that attestationCert is issued to the hostname "attest.android.com" - $certInfo = \openssl_x509_parse($this->getCertificatePem()); - if (!\is_array($certInfo) || !$certInfo['subject'] || $certInfo['subject']['CN'] !== 'attest.android.com') { - throw new WebAuthnException('invalid certificate CN in JWS (' . $certInfo['subject']['CN']. ')', WebAuthnException::INVALID_DATA); - } - - // Verify that the ctsProfileMatch attribute in the payload of response is true. - if (!$this->_payload->ctsProfileMatch) { - throw new WebAuthnException('invalid ctsProfileMatch in payload', WebAuthnException::INVALID_DATA); - } - - // check certificate - return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; - } - - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - $chainC = $this->_createX5cChainFile(); - if ($chainC) { - $rootCas[] = $chainC; - } - - $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); - if ($v === -1) { - throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - return $v; - } - - - /** - * decode base64 url - * @param string $data - * @return string - */ - private function _base64url_decode($data) { - return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4)); - } -} - diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/FormatBase.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/FormatBase.php deleted file mode 100644 index a9048b9e..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/FormatBase.php +++ /dev/null @@ -1,183 +0,0 @@ -_attestationObject = $AttestionObject; - $this->_authenticatorData = $authenticatorData; - } - - /** - * - */ - public function __destruct() { - // delete X.509 chain certificate file after use - if (\is_file($this->_x5c_tempFile)) { - \unlink($this->_x5c_tempFile); - } - } - - /** - * returns the certificate chain in PEM format - * @return string|null - */ - public function getCertificateChain() { - if (\is_file($this->_x5c_tempFile)) { - return \file_get_contents($this->_x5c_tempFile); - } - return null; - } - - /** - * returns the key X.509 certificate in PEM format - * @return string - */ - public function getCertificatePem() { - // need to be overwritten - return null; - } - - /** - * checks validity of the signature - * @param string $clientDataHash - * @return bool - * @throws WebAuthnException - */ - public function validateAttestation($clientDataHash) { - // need to be overwritten - return false; - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - // need to be overwritten - return false; - } - - - /** - * create a PEM encoded certificate with X.509 binary data - * @param string $x5c - * @return string - */ - protected function _createCertificatePem($x5c) { - $pem = '-----BEGIN CERTIFICATE-----' . "\n"; - $pem .= \chunk_split(\base64_encode($x5c), 64, "\n"); - $pem .= '-----END CERTIFICATE-----' . "\n"; - return $pem; - } - - /** - * creates a PEM encoded chain file - * @return type - */ - protected function _createX5cChainFile() { - $content = ''; - if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) { - foreach ($this->_x5c_chain as $x5c) { - $certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c)); - // check if issuer = subject (self signed) - if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) { - $selfSigned = true; - foreach ($certInfo['issuer'] as $k => $v) { - if ($certInfo['subject'][$k] !== $v) { - $selfSigned = false; - break; - } - } - - if (!$selfSigned) { - $content .= "\n" . $this->_createCertificatePem($x5c) . "\n"; - } - } - } - } - - if ($content) { - $this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem'; - if (\file_put_contents($this->_x5c_tempFile, $content) !== false) { - return $this->_x5c_tempFile; - } - } - - return null; - } - - - /** - * returns the name and openssl key for provided cose number. - * @param int $coseNumber - * @return \stdClass|null - */ - protected function _getCoseAlgorithm($coseNumber) { - // https://www.iana.org/assignments/cose/cose.xhtml#algorithms - $coseAlgorithms = array( - array( - 'hash' => 'SHA1', - 'openssl' => OPENSSL_ALGO_SHA1, - 'cose' => array( - -65535 // RS1 - )), - - array( - 'hash' => 'SHA256', - 'openssl' => OPENSSL_ALGO_SHA256, - 'cose' => array( - -257, // RS256 - -37, // PS256 - -7, // ES256 - 5 // HMAC256 - )), - - array( - 'hash' => 'SHA384', - 'openssl' => OPENSSL_ALGO_SHA384, - 'cose' => array( - -258, // RS384 - -38, // PS384 - -35, // ES384 - 6 // HMAC384 - )), - - array( - 'hash' => 'SHA512', - 'openssl' => OPENSSL_ALGO_SHA512, - 'cose' => array( - -259, // RS512 - -39, // PS512 - -36, // ES512 - 7 // HMAC512 - )) - ); - - foreach ($coseAlgorithms as $coseAlgorithm) { - if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) { - $return = new \stdClass(); - $return->hash = $coseAlgorithm['hash']; - $return->openssl = $coseAlgorithm['openssl']; - return $return; - } - } - - return null; - } -} diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/None.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/None.php deleted file mode 100644 index 1664c559..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/None.php +++ /dev/null @@ -1,39 +0,0 @@ -_attestationObject['attStmt']; - - if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { - throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { - throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); - } - - $this->_alg = $attStmt['alg']; - $this->_signature = $attStmt['sig']->getBinaryString(); - - // certificate for validation - if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { - - // The attestation certificate attestnCert MUST be the first element in the array - $attestnCert = array_shift($attStmt['x5c']); - - if (!($attestnCert instanceof ByteBuffer)) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - $this->_x5c = $attestnCert->getBinaryString(); - - // certificate chain - foreach ($attStmt['x5c'] as $chain) { - if ($chain instanceof ByteBuffer) { - $this->_x5c_chain[] = $chain->getBinaryString(); - } - } - } - } - - - /* - * returns the key certificate in PEM format - * @return string|null - */ - public function getCertificatePem() { - if (!$this->_x5c) { - return null; - } - return $this->_createCertificatePem($this->_x5c); - } - - /** - * @param string $clientDataHash - */ - public function validateAttestation($clientDataHash) { - if ($this->_x5c) { - return $this->_validateOverX5c($clientDataHash); - } else { - return $this->_validateSelfAttestation($clientDataHash); - } - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - if (!$this->_x5c) { - return false; - } - - $chainC = $this->_createX5cChainFile(); - if ($chainC) { - $rootCas[] = $chainC; - } - - $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); - if ($v === -1) { - throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - return $v; - } - - /** - * validate if x5c is present - * @param string $clientDataHash - * @return bool - * @throws WebAuthnException - */ - protected function _validateOverX5c($clientDataHash) { - $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); - - if ($publicKey === false) { - throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); - } - - // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using the attestation public key in attestnCert with the algorithm specified in alg. - $dataToVerify = $this->_authenticatorData->getBinary(); - $dataToVerify .= $clientDataHash; - - $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); - - // check certificate - return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; - } - - /** - * validate if self attestation is in use - * @param string $clientDataHash - * @return bool - */ - protected function _validateSelfAttestation($clientDataHash) { - // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using the credential public key with alg. - $dataToVerify = $this->_authenticatorData->getBinary(); - $dataToVerify .= $clientDataHash; - - $publicKey = $this->_authenticatorData->getPublicKeyPem(); - - // check certificate - return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; - } -} - diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/Tpm.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/Tpm.php deleted file mode 100644 index 32bb7663..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/Tpm.php +++ /dev/null @@ -1,179 +0,0 @@ -_attestationObject['attStmt']; - - if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') { - throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { - throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { - throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) { - throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) { - throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA); - } - - $this->_alg = $attStmt['alg']; - $this->_signature = $attStmt['sig']->getBinaryString(); - $this->_certInfo = $attStmt['certInfo']; - $this->_pubArea = $attStmt['pubArea']; - - // certificate for validation - if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { - - // The attestation certificate attestnCert MUST be the first element in the array - $attestnCert = array_shift($attStmt['x5c']); - - if (!($attestnCert instanceof ByteBuffer)) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - $this->_x5c = $attestnCert->getBinaryString(); - - // certificate chain - foreach ($attStmt['x5c'] as $chain) { - if ($chain instanceof ByteBuffer) { - $this->_x5c_chain[] = $chain->getBinaryString(); - } - } - - } else { - throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA); - } - } - - - /* - * returns the key certificate in PEM format - * @return string|null - */ - public function getCertificatePem() { - if (!$this->_x5c) { - return null; - } - return $this->_createCertificatePem($this->_x5c); - } - - /** - * @param string $clientDataHash - */ - public function validateAttestation($clientDataHash) { - return $this->_validateOverX5c($clientDataHash); - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - if (!$this->_x5c) { - return false; - } - - $chainC = $this->_createX5cChainFile(); - if ($chainC) { - $rootCas[] = $chainC; - } - - $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); - if ($v === -1) { - throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - return $v; - } - - /** - * validate if x5c is present - * @param string $clientDataHash - * @return bool - * @throws WebAuthnException - */ - protected function _validateOverX5c($clientDataHash) { - $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); - - if ($publicKey === false) { - throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); - } - - // Concatenate authenticatorData and clientDataHash to form attToBeSigned. - $attToBeSigned = $this->_authenticatorData->getBinary(); - $attToBeSigned .= $clientDataHash; - - // Validate that certInfo is valid: - - // Verify that magic is set to TPM_GENERATED_VALUE. - if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) { - throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA); - } - - // Verify that type is set to TPM_ST_ATTEST_CERTIFY. - if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) { - throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA); - } - - $offset = 6; - $qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset); - $extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset); - $coseAlg = $this->_getCoseAlgorithm($this->_alg); - - // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg". - if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) { - throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA); - } - - // Verify the sig is a valid signature over certInfo using the attestation - // public key in aikCert with the algorithm specified in alg. - return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1; - } - - - /** - * returns next part of ByteBuffer - * @param ByteBuffer $buffer - * @param int $offset - * @return ByteBuffer - */ - protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) { - $len = $buffer->getUint16Val($offset); - $data = $buffer->getBytes($offset + 2, $len); - $offset += (2 + $len); - - return new ByteBuffer($data); - } - -} - diff --git a/data/web/inc/lib/WebAuthn_/Attestation/Format/U2f.php b/data/web/inc/lib/WebAuthn_/Attestation/Format/U2f.php deleted file mode 100644 index 630b1544..00000000 --- a/data/web/inc/lib/WebAuthn_/Attestation/Format/U2f.php +++ /dev/null @@ -1,93 +0,0 @@ -_attestationObject['attStmt']; - - if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { - throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { - throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); - } - - if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { - throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); - } - - $this->_alg = $attStmt['alg']; - $this->_signature = $attStmt['sig']->getBinaryString(); - $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); - } - - - /* - * returns the key certificate in PEM format - * @return string - */ - public function getCertificatePem() { - $pem = '-----BEGIN CERTIFICATE-----' . "\n"; - $pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n"); - $pem .= '-----END CERTIFICATE-----' . "\n"; - return $pem; - } - - /** - * @param string $clientDataHash - */ - public function validateAttestation($clientDataHash) { - $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); - - if ($publicKey === false) { - throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); - } - - // Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) - $dataToVerify = "\x00"; - $dataToVerify .= $this->_authenticatorData->getRpIdHash(); - $dataToVerify .= $clientDataHash; - $dataToVerify .= $this->_authenticatorData->getCredentialId(); - $dataToVerify .= $this->_authenticatorData->getPublicKeyU2F(); - - $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); - - // check certificate - return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; - } - - /** - * validates the certificate against root certificates - * @param array $rootCas - * @return boolean - * @throws WebAuthnException - */ - public function validateRootCertificate($rootCas) { - $chainC = $this->_createX5cChainFile(); - if ($chainC) { - $rootCas[] = $chainC; - } - - $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); - if ($v === -1) { - throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - return $v; - } -} diff --git a/data/web/inc/lib/WebAuthn_/Binary/ByteBuffer.php b/data/web/inc/lib/WebAuthn_/Binary/ByteBuffer.php deleted file mode 100644 index dd0eec7b..00000000 --- a/data/web/inc/lib/WebAuthn_/Binary/ByteBuffer.php +++ /dev/null @@ -1,255 +0,0 @@ -_data = $binaryData; - $this->_length = \strlen($binaryData); - } - - - // ----------------------- - // PUBLIC STATIC - // ----------------------- - - /** - * create a ByteBuffer from a base64 url encoded string - * @param string $base64url - * @return \WebAuthn\Binary\ByteBuffer - */ - public static function fromBase64Url($base64url) { - $bin = self::_base64url_decode($base64url); - if ($bin === false) { - throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER); - } - return new ByteBuffer($bin); - } - - /** - * create a ByteBuffer from a base64 url encoded string - * @param string $hex - * @return \WebAuthn\Binary\ByteBuffer - */ - public static function fromHex($hex) { - $bin = \hex2bin($hex); - if ($bin === false) { - throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER); - } - return new ByteBuffer($bin); - } - - /** - * create a random ByteBuffer - * @param string $length - * @return \WebAuthn\Binary\ByteBuffer - */ - public static function randomBuffer($length) { - if (\function_exists('random_bytes')) { // >PHP 7.0 - return new ByteBuffer(\random_bytes($length)); - - } else if (\function_exists('openssl_random_pseudo_bytes')) { - return new ByteBuffer(\openssl_random_pseudo_bytes($length)); - - } else { - throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER); - } - } - - // ----------------------- - // PUBLIC - // ----------------------- - - public function getBytes($offset, $length) { - if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) { - throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER); - } - return \substr($this->_data, $offset, $length); - } - - public function getByteVal($offset) { - if ($offset < 0 || $offset >= $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - return \ord(\substr($this->_data, $offset, 1)); - } - - public function getLength() { - return $this->_length; - } - - public function getUint16Val($offset) { - if ($offset < 0 || ($offset + 2) > $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - return unpack('n', $this->_data, $offset)[1]; - } - - public function getUint32Val($offset) { - if ($offset < 0 || ($offset + 4) > $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - $val = unpack('N', $this->_data, $offset)[1]; - - // Signed integer overflow causes signed negative numbers - if ($val < 0) { - throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER); - } - return $val; - } - - public function getUint64Val($offset) { - if (PHP_INT_SIZE < 8) { - throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER); - } - if ($offset < 0 || ($offset + 8) > $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - $val = unpack('J', $this->_data, $offset)[1]; - - // Signed integer overflow causes signed negative numbers - if ($val < 0) { - throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER); - } - - return $val; - } - - public function getHalfFloatVal($offset) { - //FROM spec pseudo decode_half(unsigned char *halfp) - $half = $this->getUint16Val($offset); - - $exp = ($half >> 10) & 0x1f; - $mant = $half & 0x3ff; - - if ($exp === 0) { - $val = $mant * (2 ** -24); - } elseif ($exp !== 31) { - $val = ($mant + 1024) * (2 ** ($exp - 25)); - } else { - $val = ($mant === 0) ? INF : NAN; - } - - return ($half & 0x8000) ? -$val : $val; - } - - public function getFloatVal($offset) { - if ($offset < 0 || ($offset + 4) > $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - return unpack('G', $this->_data, $offset)[1]; - } - - public function getDoubleVal($offset) { - if ($offset < 0 || ($offset + 8) > $this->_length) { - throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); - } - return unpack('E', $this->_data, $offset)[1]; - } - - /** - * @return string - */ - public function getBinaryString() { - return $this->_data; - } - - /** - * @param string $buffer - * @return bool - */ - public function equals($buffer) { - return is_string($this->_data) && $this->_data === $buffer->data; - } - - /** - * @return string - */ - public function getHex() { - return \bin2hex($this->_data); - } - - /** - * @return bool - */ - public function isEmpty() { - return $this->_length === 0; - } - - - /** - * jsonSerialize interface - * return binary data in RFC 1342-Like serialized string - * @return \stdClass - */ - public function jsonSerialize() { - if (ByteBuffer::$useBase64UrlEncoding) { - return self::_base64url_encode($this->_data); - - } else { - return '=?BINARY?B?' . \base64_encode($this->_data) . '?='; - } - } - - /** - * Serializable-Interface - * @return string - */ - public function serialize() { - return \serialize($this->_data); - } - - /** - * Serializable-Interface - * @param string $serialized - */ - public function unserialize($serialized) { - $this->_data = \unserialize($serialized); - $this->_length = \strlen($this->_data); - } - - // ----------------------- - // PROTECTED STATIC - // ----------------------- - - /** - * base64 url decoding - * @param string $data - * @return string - */ - protected static function _base64url_decode($data) { - return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4)); - } - - /** - * base64 url encoding - * @param string $data - * @return string - */ - protected static function _base64url_encode($data) { - return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '='); - } -} diff --git a/data/web/inc/lib/WebAuthn_/CBOR/CborDecoder.php b/data/web/inc/lib/WebAuthn_/CBOR/CborDecoder.php deleted file mode 100644 index 45626eb1..00000000 --- a/data/web/inc/lib/WebAuthn_/CBOR/CborDecoder.php +++ /dev/null @@ -1,220 +0,0 @@ -getLength()) { - throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR); - } - return $result; - } - - /** - * @param ByteBuffer|string $bufOrBin - * @param int $startOffset - * @param int|null $endOffset - * @return mixed - */ - public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) { - $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin); - - $offset = $startOffset; - $data = self::_parseItem($buf, $offset); - $endOffset = $offset; - return $data; - } - - // --------------------- - // protected - // --------------------- - - /** - * @param ByteBuffer $buf - * @param int $offset - * @return mixed - */ - protected static function _parseItem(ByteBuffer $buf, &$offset) { - $first = $buf->getByteVal($offset++); - $type = $first >> 5; - $val = $first & 0b11111; - - if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) { - return self::_parseFloatSimple($val, $buf, $offset); - } - - $val = self::_parseExtraLength($val, $buf, $offset); - - return self::_parseItemData($type, $val, $buf, $offset); - } - - protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) { - switch ($val) { - case 24: - $val = $buf->getByteVal($offset); - $offset++; - return self::_parseSimple($val); - - case 25: - $floatValue = $buf->getHalfFloatVal($offset); - $offset += 2; - return $floatValue; - - case 26: - $floatValue = $buf->getFloatVal($offset); - $offset += 4; - return $floatValue; - - case 27: - $floatValue = $buf->getDoubleVal($offset); - $offset += 8; - return $floatValue; - - case 28: - case 29: - case 30: - throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR); - - case 31: - throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR); - } - - return self::_parseSimple($val); - } - - /** - * @param int $val - * @return mixed - * @throws WebAuthnException - */ - protected static function _parseSimple($val) { - if ($val === 20) { - return false; - } - if ($val === 21) { - return true; - } - if ($val === 22) { - return null; - } - throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR); - } - - protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) { - switch ($val) { - case 24: - $val = $buf->getByteVal($offset); - $offset++; - break; - - case 25: - $val = $buf->getUint16Val($offset); - $offset += 2; - break; - - case 26: - $val = $buf->getUint32Val($offset); - $offset += 4; - break; - - case 27: - $val = $buf->getUint64Val($offset); - $offset += 8; - break; - - case 28: - case 29: - case 30: - throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR); - - case 31: - throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR); - } - - return $val; - } - - protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) { - switch ($type) { - case self::CBOR_MAJOR_UNSIGNED_INT: // uint - return $val; - - case self::CBOR_MAJOR_NEGATIVE_INT: - return -1 - $val; - - case self::CBOR_MAJOR_BYTE_STRING: - $data = $buf->getBytes($offset, $val); - $offset += $val; - return new ByteBuffer($data); // bytes - - case self::CBOR_MAJOR_TEXT_STRING: - $data = $buf->getBytes($offset, $val); - $offset += $val; - return $data; // UTF-8 - - case self::CBOR_MAJOR_ARRAY: - return self::_parseArray($buf, $offset, $val); - - case self::CBOR_MAJOR_MAP: - return self::_parseMap($buf, $offset, $val); - - case self::CBOR_MAJOR_TAG: - return self::_parseItem($buf, $offset); // 1 embedded data item - } - - // This should never be reached - throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR); - } - - protected static function _parseMap(ByteBuffer $buf, &$offset, $count) { - $map = array(); - - for ($i = 0; $i < $count; $i++) { - $mapKey = self::_parseItem($buf, $offset); - $mapVal = self::_parseItem($buf, $offset); - - if (!\is_int($mapKey) && !\is_string($mapKey)) { - throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR); - } - - $map[$mapKey] = $mapVal; // todo dup - } - return $map; - } - - protected static function _parseArray(ByteBuffer $buf, &$offset, $count) { - $arr = array(); - for ($i = 0; $i < $count; $i++) { - $arr[] = self::_parseItem($buf, $offset); - } - - return $arr; - } -} diff --git a/data/web/inc/lib/WebAuthn_/LICENSE b/data/web/inc/lib/WebAuthn_/LICENSE deleted file mode 100644 index e24a2b63..00000000 --- a/data/web/inc/lib/WebAuthn_/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright © 2019 Lukas Buchs -Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part) - -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. diff --git a/data/web/inc/lib/WebAuthn_/WebAuthn.php b/data/web/inc/lib/WebAuthn_/WebAuthn.php deleted file mode 100644 index a685fac8..00000000 --- a/data/web/inc/lib/WebAuthn_/WebAuthn.php +++ /dev/null @@ -1,487 +0,0 @@ -_rpName = $rpName; - $this->_rpId = $rpId; - $this->_rpIdHash = \hash('sha256', $rpId, true); - ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding; - - if (!\function_exists('\openssl_open')) { - throw new WebAuthnException('OpenSSL-Module not installed');; - } - - if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) { - throw new WebAuthnException('SHA256 not supported by this openssl installation.'); - } - - // default value - if (!is_array($allowedFormats)) { - $allowedFormats = array('android-key', 'fido-u2f', 'packed', 'tpm'); - } - $this->_formats = $allowedFormats; - - // validate formats - $invalidFormats = \array_diff($this->_formats, array('android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm')); - if (!$this->_formats || $invalidFormats) { - throw new WebAuthnException('Invalid formats on construct: ' . implode(', ', $invalidFormats)); - } - } - - /** - * add a root certificate to verify new registrations - * @param string $path file path of / directory with root certificates - */ - public function addRootCertificates($path) { - if (!\is_array($this->_caFiles)) { - $this->_caFiles = array(); - } - $path = \rtrim(\trim($path), '\\/'); - if (\is_dir($path)) { - foreach (\scandir($path) as $ca) { - if (\is_file($path . '/' . $ca)) { - $this->addRootCertificates($path . '/' . $ca); - } - } - } else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) { - $this->_caFiles[] = \realpath($path); - } - } - - /** - * Returns the generated challenge to save for later validation - * @return ByteBuffer - */ - public function getChallenge() { - return $this->_challenge; - } - - /** - * generates the object for a key registration - * provide this data to navigator.credentials.create - * @param string $userId - * @param string $userName - * @param string $userDisplayName - * @param int $timeout timeout in seconds - * @param bool $requireResidentKey true, if the key should be stored by the authentication device - * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation - * if the response does not have the UV flag set. - * Valid values: - * true = required - * false = preferred - * string 'required' 'preferred' 'discouraged' - * @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration - * @return \stdClass - */ - public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $excludeCredentialIds=array()) { - - // validate User Verification Requirement - if (\is_bool($requireUserVerification)) { - $requireUserVerification = $requireUserVerification ? 'required' : 'preferred'; - } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) { - $requireUserVerification = \strtolower($requireUserVerification); - } else { - $requireUserVerification = 'preferred'; - } - - $args = new \stdClass(); - $args->publicKey = new \stdClass(); - - // relying party - $args->publicKey->rp = new \stdClass(); - $args->publicKey->rp->name = $this->_rpName; - $args->publicKey->rp->id = $this->_rpId; - - $args->publicKey->authenticatorSelection = new \stdClass(); - $args->publicKey->authenticatorSelection->userVerification = $requireUserVerification; - if ($requireResidentKey) { - $args->publicKey->authenticatorSelection->requireResidentKey = true; - } - - // user - $args->publicKey->user = new \stdClass(); - $args->publicKey->user->id = new ByteBuffer($userId); // binary - $args->publicKey->user->name = $userName; - $args->publicKey->user->displayName = $userDisplayName; - - $args->publicKey->pubKeyCredParams = array(); - $tmp = new \stdClass(); - $tmp->type = 'public-key'; - $tmp->alg = -7; // ES256 - $args->publicKey->pubKeyCredParams[] = $tmp; - unset ($tmp); - - $tmp = new \stdClass(); - $tmp->type = 'public-key'; - $tmp->alg = -257; // RS256 - $args->publicKey->pubKeyCredParams[] = $tmp; - unset ($tmp); - - // if there are root certificates added, we need direct attestation to validate - // against the root certificate. If there are no root-certificates added, - // anonymization ca are also accepted, because we can't validate the root anyway. - $attestation = 'indirect'; - if (\is_array($this->_caFiles)) { - $attestation = 'direct'; - } - - $args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation; - $args->publicKey->extensions = new \stdClass(); - $args->publicKey->extensions->exts = true; - $args->publicKey->timeout = $timeout * 1000; // microseconds - $args->publicKey->challenge = $this->_createChallenge(); // binary - - //prevent re-registration by specifying existing credentials - $args->publicKey->excludeCredentials = array(); - - if (is_array($excludeCredentialIds)) { - foreach ($excludeCredentialIds as $id) { - $tmp = new \stdClass(); - $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary - $tmp->type = 'public-key'; - $tmp->transports = array('usb', 'ble', 'nfc', 'internal'); - $args->publicKey->excludeCredentials[] = $tmp; - unset ($tmp); - } - } - - return $args; - } - - /** - * generates the object for key validation - * Provide this data to navigator.credentials.get - * @param array $credentialIds binary - * @param int $timeout timeout in seconds - * @param bool $allowUsb allow removable USB - * @param bool $allowNfc allow Near Field Communication (NFC) - * @param bool $allowBle allow Bluetooth - * @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device. - * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation - * if the response does not have the UV flag set. - * Valid values: - * true = required - * false = preferred - * string 'required' 'preferred' 'discouraged' - * @return \stdClass - */ - public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowInternal=true, $requireUserVerification=false) { - - // validate User Verification Requirement - if (\is_bool($requireUserVerification)) { - $requireUserVerification = $requireUserVerification ? 'required' : 'preferred'; - } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) { - $requireUserVerification = \strtolower($requireUserVerification); - } else { - $requireUserVerification = 'preferred'; - } - - $args = new \stdClass(); - $args->publicKey = new \stdClass(); - $args->publicKey->timeout = $timeout * 1000; // microseconds - $args->publicKey->challenge = $this->_createChallenge(); // binary - $args->publicKey->userVerification = $requireUserVerification; - $args->publicKey->rpId = $this->_rpId; - - if (\is_array($credentialIds) && \count($credentialIds) > 0) { - $args->publicKey->allowCredentials = array(); - - foreach ($credentialIds as $id) { - $tmp = new \stdClass(); - $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary - $tmp->transports = array(); - - if ($allowUsb) { - $tmp->transports[] = 'usb'; - } - if ($allowNfc) { - $tmp->transports[] = 'nfc'; - } - if ($allowBle) { - $tmp->transports[] = 'ble'; - } - if ($allowInternal) { - $tmp->transports[] = 'internal'; - } - - $tmp->type = 'public-key'; - $args->publicKey->allowCredentials[] = $tmp; - unset ($tmp); - } - } - - return $args; - } - - /** - * returns the new signature counter value. - * returns null if there is no counter - * @return ?int - */ - public function getSignatureCounter() { - return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null; - } - - /** - * process a create request and returns data to save for future logins - * @param string $clientDataJSON binary from browser - * @param string $attestationObject binary from browser - * @param string|ByteBuffer $challenge binary used challange - * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin) - * @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button) - * @return \stdClass - * @throws WebAuthnException - */ - public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true) { - $clientDataHash = \hash('sha256', $clientDataJSON, true); - $clientData = \json_decode($clientDataJSON); - $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge); - - // security: https://www.w3.org/TR/webauthn/#registering-a-new-credential - - // 2. Let C, the client data claimed as collected during the credential creation, - // be the result of running an implementation-specific JSON parser on JSONtext. - if (!\is_object($clientData)) { - throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA); - } - - // 3. Verify that the value of C.type is webauthn.create. - if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') { - throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE); - } - - // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. - if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) { - throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE); - } - - // 5. Verify that the value of C.origin matches the Relying Party's origin. - if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) { - throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN); - } - - // Attestation - $attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats); - - // 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP. - if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) { - throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY); - } - - // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature - if (!$attestationObject->validateAttestation($clientDataHash)) { - throw new WebAuthnException('Invalid certificate signature', WebAuthnException::INVALID_SIGNATURE); - } - - // 15. If validation is successful, obtain a list of acceptable trust anchors - if (is_array($this->_caFiles) && !$attestationObject->validateRootCertificate($this->_caFiles)) { - throw new WebAuthnException('Invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED); - } - - // 10. Verify that the User Present bit of the flags in authData is set. - if ($requireUserPresent && !$attestationObject->getAuthenticatorData()->getUserPresent()) { - throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT); - } - - // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. - if ($requireUserVerification && !$attestationObject->getAuthenticatorData()->getUserVerified()) { - throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED); - } - - $signCount = $attestationObject->getAuthenticatorData()->getSignCount(); - if ($signCount > 0) { - $this->_signatureCounter = $signCount; - } - - // prepare data to store for future logins - $data = new \stdClass(); - $data->rpId = $this->_rpId; - $data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId(); - $data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem(); - $data->certificateChain = $attestationObject->getCertificateChain(); - $data->certificate = $attestationObject->getCertificatePem(); - $data->certificateIssuer = $attestationObject->getCertificateIssuer(); - $data->certificateSubject = $attestationObject->getCertificateSubject(); - $data->signatureCounter = $this->_signatureCounter; - $data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID(); - return $data; - } - - - /** - * process a get request - * @param string $clientDataJSON binary from browser - * @param string $authenticatorData binary from browser - * @param string $signature binary from browser - * @param string $credentialPublicKey string PEM-formated public key from used credentialId - * @param string|ByteBuffer $challenge binary from used challange - * @param int $prevSignatureCnt signature count value of the last login - * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin) - * @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button) - * @return boolean true if get is successful - * @throws WebAuthnException - */ - public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) { - $authenticatorObj = new Attestation\AuthenticatorData($authenticatorData); - $clientDataHash = \hash('sha256', $clientDataJSON, true); - $clientData = \json_decode($clientDataJSON); - $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge); - - // https://www.w3.org/TR/webauthn/#verifying-assertion - - // 1. If the allowCredentials option was given when this authentication ceremony was initiated, - // verify that credential.id identifies one of the public key credentials that were listed in allowCredentials. - // -> TO BE VERIFIED BY IMPLEMENTATION - - // 2. If credential.response.userHandle is present, verify that the user identified - // by this value is the owner of the public key credential identified by credential.id. - // -> TO BE VERIFIED BY IMPLEMENTATION - - // 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is - // inappropriate for your use case), look up the corresponding credential public key. - // -> TO BE LOOKED UP BY IMPLEMENTATION - - // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. - if (!\is_object($clientData)) { - throw new WebAuthnException('Invalid client data', WebAuthnException::INVALID_DATA); - } - - // 7. Verify that the value of C.type is the string webauthn.get. - if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') { - throw new WebAuthnException('Invalid type', WebAuthnException::INVALID_TYPE); - } - - // 8. Verify that the value of C.challenge matches the challenge that was sent to the - // authenticator in the PublicKeyCredentialRequestOptions passed to the get() call. - if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) { - throw new WebAuthnException('Invalid challenge', WebAuthnException::INVALID_CHALLENGE); - } - - // 9. Verify that the value of C.origin matches the Relying Party's origin. - if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) { - throw new WebAuthnException('Invalid origin', WebAuthnException::INVALID_ORIGIN); - } - - // 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party. - if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) { - throw new WebAuthnException('Invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY); - } - - // 12. Verify that the User Present bit of the flags in authData is set - if ($requireUserPresent && !$authenticatorObj->getUserPresent()) { - throw new WebAuthnException('User not present during authentication', WebAuthnException::USER_PRESENT); - } - - // 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set. - if ($requireUserVerification && !$authenticatorObj->getUserVerified()) { - throw new WebAuthnException('User not verificated during authentication', WebAuthnException::USER_VERIFICATED); - } - - // 14. Verify the values of the client extension outputs - // (extensions not implemented) - - // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature - // over the binary concatenation of authData and hash. - $dataToVerify = ''; - $dataToVerify .= $authenticatorData; - $dataToVerify .= $clientDataHash; - - $publicKey = \openssl_pkey_get_public($credentialPublicKey); - if ($publicKey === false) { - throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY); - } - - if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) { - throw new WebAuthnException('Invalid signature', WebAuthnException::INVALID_SIGNATURE); - } - - // 17. If the signature counter value authData.signCount is nonzero, - // if less than or equal to the signature counter value stored, - // is a signal that the authenticator may be cloned - $signatureCounter = $authenticatorObj->getSignCount(); - if ($signatureCounter > 0) { - $this->_signatureCounter = $signatureCounter; - if ($prevSignatureCnt !== null && $prevSignatureCnt >= $signatureCounter) { - throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER); - } - } - - return true; - } - - // ----------------------------------------------- - // PRIVATE - // ----------------------------------------------- - - /** - * checks if the origin matchs the RP ID - * @param string $origin - * @return boolean - * @throws WebAuthnException - */ - private function _checkOrigin($origin) { - // https://www.w3.org/TR/webauthn/#rp-id - - // The origin's scheme must be https - if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') { - return false; - } - - // extract host from origin - $host = \parse_url($origin, PHP_URL_HOST); - $host = \trim($host, '.'); - - // The RP ID must be equal to the origin's effective domain, or a registrable - // domain suffix of the origin's effective domain. - return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1; - } - - /** - * generates a new challange - * @param int $length - * @return string - * @throws WebAuthnException - */ - private function _createChallenge($length = 32) { - if (!$this->_challenge) { - $this->_challenge = ByteBuffer::randomBuffer($length); - } - return $this->_challenge; - } -} diff --git a/data/web/inc/lib/WebAuthn_/WebAuthnException.php b/data/web/inc/lib/WebAuthn_/WebAuthnException.php deleted file mode 100644 index 823f7d80..00000000 --- a/data/web/inc/lib/WebAuthn_/WebAuthnException.php +++ /dev/null @@ -1,27 +0,0 @@ -