commit ec96efb55bfdab795ed234c33bcbd962b2470a4f Author: Janis Steiner <89935073+jakani24@users.noreply.github.com> Date: Wed Jun 26 16:50:09 2024 +0200 Add files via upload diff --git a/server/apache-conf/000-default.conf b/server/apache-conf/000-default.conf new file mode 100644 index 0000000..bbcdc62 --- /dev/null +++ b/server/apache-conf/000-default.conf @@ -0,0 +1,35 @@ + + ServerName cyberhex.srv + DocumentRoot /var/www/html + + #SSLEngine on + #SSLCertificateFile /etc/apache2/certs/fullchain.pem + #SSLCertificateKeyFile /etc/apache2/certs/privkey.pem + #SSLCertificateChainFile /etc/apache2/certs/chain.pem + + + Options FollowSymLinks + AllowOverride All + Require all granted + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + + ServerName cyberhex.srv + DocumentRoot /var/www/html + + SSLEngine on + SSLCertificateKeyFile /etc/apache2/certs/privkey.pem + SSLCertificateFile /etc/apache2/certs/fullchain.pem +# SSLCertificateChainFile /etc/apache2/certs/fullchain.pem + + + Options FollowSymLinks + AllowOverride All + Require all granted + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/server/app-code/config.php b/server/app-code/config.php new file mode 100644 index 0000000..6889495 --- /dev/null +++ b/server/app-code/config.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/server/app-code/index.php b/server/app-code/index.php new file mode 100644 index 0000000..769364c --- /dev/null +++ b/server/app-code/index.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/server/app-code/install.bat b/server/app-code/install.bat new file mode 100644 index 0000000..8fa4779 --- /dev/null +++ b/server/app-code/install.bat @@ -0,0 +1,21 @@ +@echo off +echo "Downloading Cyberhex installer" +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ma_installer.exe +echo "Downloading dll files" +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcrypto-3-x64.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcurl.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/zlib1.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/client_frontend.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/client_backend.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ma_uninstaller.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcurl-d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/msvcp140.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140_1d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/msvcp140d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ucrtbased.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/zlibd1.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/cyberhex_logo2.ico +echo "Download finished, starting installer" +start ma_installer.exe diff --git a/server/app-code/install/add_passkey.php b/server/app-code/install/add_passkey.php new file mode 100644 index 0000000..c52bf68 --- /dev/null +++ b/server/app-code/install/add_passkey.php @@ -0,0 +1,229 @@ + + + + + + + + Add passkey + + + +
+
+
+
+
+

Add a passkey?

+
+
+ " style="display: none;"> +

You can add a device specific passkey which allows you to login in securely with your fingerprint / hardware key etc.

+ +

+ Skip for now + +
+
+
+
+
+ + diff --git a/server/app-code/install/create_admin.php b/server/app-code/install/create_admin.php new file mode 100644 index 0000000..f53aab0 --- /dev/null +++ b/server/app-code/install/create_admin.php @@ -0,0 +1,85 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Add an admin user

+
+
+

Please create an initial admin user. This user the can create new users etc.
Please use a strong password for this user!

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + //create the unauth user + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES ('unauth@cyberhex.local', 'not_authenticated_user', 'this_user_does_not_have_a_password', '000000000000000',0,0,0)"); + $stmt->execute(); + $stmt->close(); + + + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES (?, ?, ?, ?,1,0,0)"); + $stmt->bind_param("ssss", $email, $username, $hash, $permissions); + + $email=htmlspecialchars($_POST["email"]); + $username=htmlspecialchars($_POST["username"]); + $password=$_POST["password"]; + $permissions="11111111111"; + $hash=password_hash($password, PASSWORD_BCRYPT); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + echo '
'; + } + + ?> +
+
+
+
+
+ + diff --git a/server/app-code/install/create_db.php b/server/app-code/install/create_db.php new file mode 100644 index 0000000..6f42dd9 --- /dev/null +++ b/server/app-code/install/create_db.php @@ -0,0 +1,469 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

We are creating the databases used in cyberhex, please stand by

+
+
+

If the creation fails, please wait a minute and try again. The database server might still be starting at the time.

+
+
+ connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + // Create database + $sql = "CREATE DATABASE IF NOT EXISTS $DB_DATABASE"; + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + $conn->close(); + + // Connect to the new database + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + + // Check connection + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + // Create user table + $sql = "CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + perms VARCHAR(255), + password VARCHAR(255), + 2fa VARCHAR(255), + telegram_id VARCHAR(255), + user_hex_id VARCHAR(255), + credential_id VARBINARY(64), + allow_pw_login INT, + use_2fa INT, + send_login_message INT, + public_key TEXT, + counter INT + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create log table + $sql = "CREATE TABLE IF NOT EXISTS log ( + id INT AUTO_INCREMENT PRIMARY KEY, + logtext VARCHAR(500) NOT NULL, + loglevel VARCHAR(255) NOT NULL, + machine_id VARCHAR(255), + time VARCHAR(255) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create incident table + $sql = "CREATE TABLE IF NOT EXISTS incidents ( + id INT AUTO_INCREMENT PRIMARY KEY, + status VARCHAR(50) NOT NULL, + description VARCHAR(255) NOT NULL, + opened VARCHAR(50), + closed VARCHAR(50) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create todo_items table + $sql = "CREATE TABLE IF NOT EXISTS todo_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + done INT, + done_by INT, + belongs_to_list INT, + text VARCHAR(500) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create todo lists table + $sql = "CREATE TABLE IF NOT EXISTS todo_lists ( + id INT AUTO_INCREMENT PRIMARY KEY, + belongs_to_incident INT, + name VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create chat table + $sql = "CREATE TABLE IF NOT EXISTS chats ( + id INT AUTO_INCREMENT PRIMARY KEY, + belongs_to_incident INT, + text TEXT NOT NULL, + sent VARCHAR(50), + from_userid INT + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + + // Create server log table + $sql = "CREATE TABLE IF NOT EXISTS server_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + logtext VARCHAR(500) NOT NULL, + loglevel VARCHAR(255) NOT NULL, + userid INT, + time VARCHAR(255) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create settings table + $sql = "CREATE TABLE IF NOT EXISTS settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + value VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //user tasks table + $sql = "CREATE TABLE IF NOT EXISTS user_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + task VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //system tasks table + $sql = "CREATE TABLE IF NOT EXISTS system_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + task VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + // Create rtp_included table + $sql = "CREATE TABLE IF NOT EXISTS rtp_included ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + // Create rtp_excluded table + $sql = "CREATE TABLE IF NOT EXISTS rtp_excluded ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create dissalowed_start table + $sql = "CREATE TABLE IF NOT EXISTS disallowed_start ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create api table + $sql = "CREATE TABLE IF NOT EXISTS api ( + id INT AUTO_INCREMENT PRIMARY KEY, + apikey VARCHAR(500) NOT NULL, + machine_id VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create secrets table + $sql = "CREATE TABLE IF NOT EXISTS secrets ( + id INT AUTO_INCREMENT PRIMARY KEY, + cert VARCHAR(500) NOT NULL, + machine_id VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create machine table + $sql = "CREATE TABLE IF NOT EXISTS machines ( + id INT AUTO_INCREMENT PRIMARY KEY, + machine_name VARCHAR(255) NOT NULL, + machine_location VARCHAR(255) NOT NULL, + machine_ip VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create vir_notify messages table + $sql = "CREATE TABLE IF NOT EXISTS vir_notify ( + id INT AUTO_INCREMENT PRIMARY KEY, + machine_id VARCHAR(255) NOT NULL, + path VARCHAR(255) NOT NULL, + hash VARCHAR(255) NOT NULL, + action VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create sig_ex messages table + $sql = "CREATE TABLE IF NOT EXISTS sig_ex ( + id INT AUTO_INCREMENT PRIMARY KEY, + signature VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create sig_in messages table + $sql = "CREATE TABLE IF NOT EXISTS sig_in ( + id INT AUTO_INCREMENT PRIMARY KEY, + signature VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Attempt to create the directory where export files will be stored later on + /*if (mkdir("/var/www/html/export", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //Attempt to create the directory where import files will be stored later on + if (mkdir("/var/www/html/import", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + //Attempt to create the directory where log backup files will be stored later on + if (mkdir("/var/www/html/backup", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + }*/ + + if($success!==1){ + echo '
'; + }else{ + echo '
'; + } + + $conn->close(); + ?> +
+
+
+
+ + diff --git a/server/app-code/install/end.php b/server/app-code/install/end.php new file mode 100644 index 0000000..a5ffb83 --- /dev/null +++ b/server/app-code/install/end.php @@ -0,0 +1,49 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

You have installed cyberhex! Thank you for choosing us!

+
+
+ Finish installation. + '; + }else{ + echo '
'; + } + } + ?> +
+
+
+
+
+ + \ No newline at end of file diff --git a/server/app-code/install/welcome.php b/server/app-code/install/welcome.php new file mode 100644 index 0000000..30e84bf --- /dev/null +++ b/server/app-code/install/welcome.php @@ -0,0 +1,30 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

Welcome to the Cyberhex Installation

+
+
+ +

The installer will guide you through the installation.

+

If there are any errors during installation or you are stuck, please contatact info.jakach@gmail.com

+ Start installation. +
+
+
+
+
+
+ + diff --git a/server/app-code/login.php b/server/app-code/login.php new file mode 100644 index 0000000..c9c38cf --- /dev/null +++ b/server/app-code/login.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/server/app-code/logo.png b/server/app-code/logo.png new file mode 100644 index 0000000..4e2f938 Binary files /dev/null and b/server/app-code/logo.png differ diff --git a/server/app-code/logout.php b/server/app-code/logout.php new file mode 100644 index 0000000..6153a36 --- /dev/null +++ b/server/app-code/logout.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/2fa.php b/server/app-code/system/insecure_zone/php/2fa.php new file mode 100644 index 0000000..3624a4e --- /dev/null +++ b/server/app-code/system/insecure_zone/php/2fa.php @@ -0,0 +1,90 @@ + + + + + + + + Cyberhex login page + + + +
+
+
+
+
+
+

Login to Cyberhex using second factor

+
+
+
+
+ + +
+
+
+ +
+
+
+ + window.location.href = "/system/secure_zone/php/index.php";'; + }else { + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + $username=$_SESSION["username"]; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + echo ''; + } + } + ?> + +
+
+
+
+
+
+ + diff --git a/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php b/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php new file mode 100644 index 0000000..65151ea --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php @@ -0,0 +1,179 @@ +_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString()); + $this->_attestationFormatName = $enc['fmt']; + + // Format ok? + if (!in_array($this->_attestationFormatName, $allowedFormats)) { + throw new WebAuthnException('invalid atttestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA); + } + + + switch ($this->_attestationFormatName) { + 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 'apple': $this->_attestationFormat = new Format\Apple($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 format name + * @return string + */ + public function getAttestationFormatName() { + return $this->_attestationFormatName; + } + + /** + * returns the attestation format class + * @return Format\FormatBase + */ + public function getAttestationFormat() { + return $this->_attestationFormat; + } + + /** + * 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) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) { + + $cn = $certInfo['issuer']['CN'] ?? ''; + $o = $certInfo['issuer']['O'] ?? ''; + $ou = $certInfo['issuer']['OU'] ?? ''; + + if ($cn) { + $issuer .= $cn; + } + if ($issuer && ($o || $ou)) { + $issuer .= ' (' . trim($o . ' ' . $ou) . ')'; + } else { + $issuer .= trim($o . ' ' . $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) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) { + + $cn = $certInfo['subject']['CN'] ?? ''; + $o = $certInfo['subject']['O'] ?? ''; + $ou = $certInfo['subject']['OU'] ?? ''; + + if ($cn) { + $subject .= $cn; + } + if ($subject && ($o || $ou)) { + $subject .= ' (' . trim($o . ' ' . $ou) . ')'; + } else { + $subject .= trim($o . ' ' . $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/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php b/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php new file mode 100644 index 0000000..0a9eec8 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php @@ -0,0 +1,481 @@ +_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() { + if (!($this->_attestedCredentialData instanceof \stdClass) || !isset($this->_attestedCredentialData->credentialPublicKey)) { + throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); + } + + $der = null; + switch ($this->_attestedCredentialData->credentialPublicKey->kty ?? null) { + case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break; + case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break; + case self::$_OKP_TYPE: $der = $this->_getOkpDer(); 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) || !isset($this->_attestedCredentialData->credentialPublicKey)) { + throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); + } + if (($this->_attestedCredentialData->credentialPublicKey->kty ?? null) !== self::$_EC2_TYPE) { + throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); + } + 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 EdDSA key + * @return string + */ + private function _getOkpDer() { + return $this->_der_sequence( + $this->_der_sequence( + $this->_der_oid("\x2B\x65\x70") // OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) + ) . + $this->_der_bitString($this->_attestedCredentialData->credentialPublicKey->x) + ); + } + + /** + * 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; + case self::$_OKP_EDDSA: $this->_readCredentialPublicKeyEDDSA($credPKey, $enc); break; + } + + return $credPKey; + } + + /** + * extract EDDSA informations from cose + * @param \stdClass $credPKey + * @param \stdClass $enc + * @throws WebAuthnException + */ + private function _readCredentialPublicKeyEDDSA(&$credPKey, $enc) { + $credPKey->crv = $enc[self::$_COSE_CRV]; + $credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null; + unset ($enc); + + // Validation + if ($credPKey->kty !== self::$_OKP_TYPE) { + throw new WebAuthnException('public key not in OKP format', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->alg !== self::$_OKP_EDDSA) { + throw new WebAuthnException('signature algorithm not EdDSA', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->crv !== self::$_OKP_ED25519) { + throw new WebAuthnException('curve not Ed25519', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->x) !== 32) { + throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); + } + } + + /** + * 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/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php new file mode 100644 index 0000000..4581272 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php @@ -0,0 +1,96 @@ +_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/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php new file mode 100644 index 0000000..ba0db52 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php @@ -0,0 +1,152 @@ +_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 (!isset($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); + } + } + + /** + * ctsProfileMatch: A stricter verdict of device integrity. + * If the value of ctsProfileMatch is true, then the profile of the device running your app matches + * the profile of a device that has passed Android compatibility testing and + * has been approved as a Google-certified Android device. + * @return bool + */ + public function ctsProfileMatch() { + return isset($this->_payload->ctsProfileMatch) ? !!$this->_payload->ctsProfileMatch : false; + } + + + /* + * 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 (empty($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']['CN'] ?? '') !== 'attest.android.com') { + throw new WebAuthnException('invalid certificate CN in JWS (' . ($certInfo['subject']['CN'] ?? '-'). ')', WebAuthnException::INVALID_DATA); + } + + // Verify that the basicIntegrity attribute in the payload of response is true. + if (empty($this->_payload->basicIntegrity)) { + throw new WebAuthnException('invalid basicIntegrity 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/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php b/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php new file mode 100644 index 0000000..e4f38e0 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php @@ -0,0 +1,139 @@ +_attestationObject['attStmt']; + + + // 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('invalid Apple attestation statement: missing x5c', WebAuthnException::INVALID_DATA); + } + } + + + /* + * returns the key certificate in PEM format + * @return string|null + */ + public function getCertificatePem() { + 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) { + $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 nonceToHash. + $nonceToHash = $this->_authenticatorData->getBinary(); + $nonceToHash .= $clientDataHash; + + // Perform SHA-256 hash of nonceToHash to produce nonce + $nonce = hash('SHA256', $nonceToHash, true); + + $credCert = openssl_x509_read($this->getCertificatePem()); + if ($credCert === false) { + throw new WebAuthnException('invalid x5c certificate: ' . \openssl_error_string(), WebAuthnException::INVALID_DATA); + } + + $keyData = openssl_pkey_get_details(openssl_pkey_get_public($credCert)); + $key = is_array($keyData) && array_key_exists('key', $keyData) ? $keyData['key'] : null; + + + // Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert. + $parsedCredCert = openssl_x509_parse($credCert); + $nonceExtension = $parsedCredCert['extensions']['1.2.840.113635.100.8.2'] ?? ''; + + // nonce padded by ASN.1 string: 30 24 A1 22 04 20 + // 30 — type tag indicating sequence + // 24 — 36 byte following + // A1 — Enumerated [1] + // 22 — 34 byte following + // 04 — type tag indicating octet string + // 20 — 32 byte following + + $asn1Padding = "\x30\x24\xA1\x22\x04\x20"; + if (substr($nonceExtension, 0, strlen($asn1Padding)) === $asn1Padding) { + $nonceExtension = substr($nonceExtension, strlen($asn1Padding)); + } + + if ($nonceExtension !== $nonce) { + throw new WebAuthnException('nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', WebAuthnException::INVALID_DATA); + } + + // Verify that the credential public key equals the Subject Public Key of credCert. + $authKeyData = openssl_pkey_get_details(openssl_pkey_get_public($this->_authenticatorData->getPublicKeyPem())); + $authKey = is_array($authKeyData) && array_key_exists('key', $authKeyData) ? $authKeyData['key'] : null; + + if ($key === null || $key !== $authKey) { + throw new WebAuthnException('credential public key doesn\'t equal the Subject Public Key of credCert', WebAuthnException::INVALID_DATA); + } + + return true; + } + +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php b/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php new file mode 100644 index 0000000..765af85 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php @@ -0,0 +1,193 @@ +_attestationObject = $AttestionObject; + $this->_authenticatorData = $authenticatorData; + } + + /** + * + */ + public function __destruct() { + // delete X.509 chain certificate file after use + if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) { + \unlink($this->_x5c_tempFile); + } + } + + /** + * returns the certificate chain in PEM format + * @return string|null + */ + public function getCertificateChain() { + if ($this->_x5c_tempFile && \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 certificate is self signed + if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) { + $selfSigned = false; + + $subjectKeyIdentifier = $certInfo['extensions']['subjectKeyIdentifier'] ?? null; + $authorityKeyIdentifier = $certInfo['extensions']['authorityKeyIdentifier'] ?? null; + + if ($authorityKeyIdentifier && substr($authorityKeyIdentifier, 0, 6) === 'keyid:') { + $authorityKeyIdentifier = substr($authorityKeyIdentifier, 6); + } + if ($subjectKeyIdentifier && substr($subjectKeyIdentifier, 0, 6) === 'keyid:') { + $subjectKeyIdentifier = substr($subjectKeyIdentifier, 6); + } + + if (($subjectKeyIdentifier && !$authorityKeyIdentifier) || ($authorityKeyIdentifier && $authorityKeyIdentifier === $subjectKeyIdentifier)) { + $selfSigned = true; + } + + if (!$selfSigned) { + $content .= "\n" . $this->_createCertificatePem($x5c) . "\n"; + } + } + } + } + + if ($content) { + $this->_x5c_tempFile = \tempnam(\sys_get_temp_dir(), 'x5c_'); + 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/server/app-code/system/insecure_zone/php/Attestation/Format/None.php b/server/app-code/system/insecure_zone/php/Attestation/Format/None.php new file mode 100644 index 0000000..ba95e40 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/None.php @@ -0,0 +1,41 @@ +_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/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php b/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php new file mode 100644 index 0000000..338cd45 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php @@ -0,0 +1,180 @@ +_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/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php b/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php new file mode 100644 index 0000000..2b51ba8 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php @@ -0,0 +1,93 @@ +_attestationObject['attStmt']; + + if (\array_key_exists('alg', $attStmt) && $attStmt['alg'] !== $this->_alg) { + throw new WebAuthnException('u2f only accepts algorithm -7 ("ES256"), but got ' . $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->_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/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php b/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php new file mode 100644 index 0000000..861ed60 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php @@ -0,0 +1,300 @@ +_data = (string)$binaryData; + $this->_length = \strlen($binaryData); + } + + + // ----------------------- + // PUBLIC STATIC + // ----------------------- + + /** + * create a ByteBuffer from a base64 url encoded string + * @param string $base64url + * @return ByteBuffer + */ + public static function fromBase64Url($base64url): ByteBuffer { + $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 ByteBuffer + */ + public static function fromHex($hex): ByteBuffer { + $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 ByteBuffer + */ + public static function randomBuffer($length): ByteBuffer { + 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): string { + 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): int { + if ($offset < 0 || $offset >= $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + return \ord(\substr($this->_data, $offset, 1)); + } + + public function getJson($jsonFlags=0) { + $data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags); + if (\json_last_error() !== JSON_ERROR_NONE) { + throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER); + } + return $data; + } + + public function getLength(): int { + 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(): string { + return $this->_data; + } + + /** + * @param string|ByteBuffer $buffer + * @return bool + */ + public function equals($buffer): bool { + if (is_object($buffer) && $buffer instanceof ByteBuffer) { + return $buffer->getBinaryString() === $this->getBinaryString(); + + } else if (is_string($buffer)) { + return $buffer === $this->getBinaryString(); + } + + return false; + } + + /** + * @return string + */ + public function getHex(): string { + return \bin2hex($this->_data); + } + + /** + * @return bool + */ + public function isEmpty(): bool { + return $this->_length === 0; + } + + + /** + * jsonSerialize interface + * return binary data in RFC 1342-Like serialized string + * @return string + */ + public function jsonSerialize(): string { + if (ByteBuffer::$useBase64UrlEncoding) { + return self::_base64url_encode($this->_data); + + } else { + return '=?BINARY?B?' . \base64_encode($this->_data) . '?='; + } + } + + /** + * Serializable-Interface + * @return string + */ + public function serialize(): string { + return \serialize($this->_data); + } + + /** + * Serializable-Interface + * @param string $serialized + */ + public function unserialize($serialized) { + $this->_data = \unserialize($serialized); + $this->_length = \strlen($this->_data); + } + + /** + * (PHP 8 deprecates Serializable-Interface) + * @return array + */ + public function __serialize(): array { + return [ + 'data' => \serialize($this->_data) + ]; + } + + /** + * object to string + * @return string + */ + public function __toString(): string { + return $this->getHex(); + } + + /** + * (PHP 8 deprecates Serializable-Interface) + * @param array $data + * @return void + */ + public function __unserialize($data) { + if ($data && isset($data['data'])) { + $this->_data = \unserialize($data['data']); + $this->_length = \strlen($this->_data); + } + } + + // ----------------------- + // PROTECTED STATIC + // ----------------------- + + /** + * base64 url decoding + * @param string $data + * @return string + */ + protected static function _base64url_decode($data): string { + 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): string { + return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '='); + } +} diff --git a/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php b/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php new file mode 100644 index 0000000..e6b5427 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php @@ -0,0 +1,220 @@ +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/server/app-code/system/insecure_zone/php/WebAuthn.php b/server/app-code/system/insecure_zone/php/WebAuthn.php new file mode 100644 index 0000000..0da0aa6 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/WebAuthn.php @@ -0,0 +1,677 @@ +_rpName = $rpName; + $this->_rpId = $rpId; + $this->_rpIdHash = \hash('sha256', $rpId, true); + ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding; + $supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm'); + + 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: all format + if (!is_array($allowedFormats)) { + $allowedFormats = $supportedFormats; + } + $this->_formats = $allowedFormats; + + // validate formats + $invalidFormats = \array_diff($this->_formats, $supportedFormats); + 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 + * @param array|null $certFileExtensions if adding a direction, all files with provided extension are added. default: pem, crt, cer, der + */ + public function addRootCertificates($path, $certFileExtensions=null) { + if (!\is_array($this->_caFiles)) { + $this->_caFiles = []; + } + if ($certFileExtensions === null) { + $certFileExtensions = array('pem', 'crt', 'cer', 'der'); + } + $path = \rtrim(\trim($path), '\\/'); + if (\is_dir($path)) { + foreach (\scandir($path) as $ca) { + if (\is_file($path . DIRECTORY_SEPARATOR . $ca) && \in_array(\strtolower(\pathinfo($ca, PATHINFO_EXTENSION)), $certFileExtensions)) { + $this->addRootCertificates($path . DIRECTORY_SEPARATOR . $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|string $requireResidentKey 'required', if the key should be stored by the authentication device + * Valid values: + * true = required + * false = preferred + * string 'required' 'preferred' 'discouraged' + * @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 bool|null $crossPlatformAttachment true for cross-platform devices (eg. fido usb), + * false for platform devices (eg. windows hello, android safetynet), + * null for both + * @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, $crossPlatformAttachment=null, $excludeCredentialIds=[]) { + + $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 = 'preferred'; + + // validate User Verification Requirement + if (\is_bool($requireUserVerification)) { + $args->publicKey->authenticatorSelection->userVerification = $requireUserVerification ? 'required' : 'preferred'; + + } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) { + $args->publicKey->authenticatorSelection->userVerification = \strtolower($requireUserVerification); + } + + // validate Resident Key Requirement + if (\is_bool($requireResidentKey) && $requireResidentKey) { + $args->publicKey->authenticatorSelection->requireResidentKey = true; + $args->publicKey->authenticatorSelection->residentKey = 'required'; + + } else if (\is_string($requireResidentKey) && \in_array(\strtolower($requireResidentKey), ['required', 'preferred', 'discouraged'])) { + $requireResidentKey = \strtolower($requireResidentKey); + $args->publicKey->authenticatorSelection->residentKey = $requireResidentKey; + $args->publicKey->authenticatorSelection->requireResidentKey = $requireResidentKey === 'required'; + } + + // filte authenticators attached with the specified authenticator attachment modality + if (\is_bool($crossPlatformAttachment)) { + $args->publicKey->authenticatorSelection->authenticatorAttachment = $crossPlatformAttachment ? 'cross-platform' : 'platform'; + } + + // user + $args->publicKey->user = new \stdClass(); + $args->publicKey->user->id = new ByteBuffer($userId); // binary + $args->publicKey->user->name = $userName; + $args->publicKey->user->displayName = $userDisplayName; + + // supported algorithms + $args->publicKey->pubKeyCredParams = []; + + if (function_exists('sodium_crypto_sign_verify_detached') || \in_array('ed25519', \openssl_get_curve_names(), true)) { + $tmp = new \stdClass(); + $tmp->type = 'public-key'; + $tmp->alg = -8; // EdDSA + $args->publicKey->pubKeyCredParams[] = $tmp; + unset ($tmp); + } + + if (\in_array('prime256v1', \openssl_get_curve_names(), true)) { + $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 = []; + + 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', 'nfc', 'ble', 'hybrid', '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 $allowHybrid allow a combination of (often separate) data-transport and proximity mechanisms. + * @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=[], $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowHybrid=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 = []; + + foreach ($credentialIds as $id) { + $tmp = new \stdClass(); + $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary + $tmp->transports = []; + + if ($allowUsb) { + $tmp->transports[] = 'usb'; + } + if ($allowNfc) { + $tmp->transports[] = 'nfc'; + } + if ($allowBle) { + $tmp->transports[] = 'ble'; + } + if ($allowHybrid) { + $tmp->transports[] = 'hybrid'; + } + 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) + * @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match + * @param bool $requireCtsProfileMatch false, if you don't want to check if the device is approved as a Google-certified Android device. + * @return \stdClass + * @throws WebAuthnException + */ + public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true, $requireCtsProfileMatch=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); + } + + // Android-SafetyNet: if required, check for Compatibility Testing Suite (CTS). + if ($requireCtsProfileMatch && $attestationObject->getAttestationFormat() instanceof Attestation\Format\AndroidSafetyNet) { + if (!$attestationObject->getAttestationFormat()->ctsProfileMatch()) { + throw new WebAuthnException('invalid ctsProfileMatch: device is not approved as a Google-certified Android device.', WebAuthnException::ANDROID_NOT_TRUSTED); + } + } + + // 15. If validation is successful, obtain a list of acceptable trust anchors + $rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null; + if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) { + throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + + // 10. Verify that the User Present bit of the flags in authData is set. + $userPresent = $attestationObject->getAuthenticatorData()->getUserPresent(); + if ($requireUserPresent && !$userPresent) { + 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. + $userVerified = $attestationObject->getAuthenticatorData()->getUserVerified(); + if ($requireUserVerification && !$userVerified) { + throw new WebAuthnException('user not verified 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->attestationFormat = $attestationObject->getAttestationFormatName(); + $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(); + $data->rootValid = $rootValid; + $data->userPresent = $userPresent; + $data->userVerified = $userVerified; + 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; + + if (!$this->_verifySignature($dataToVerify, $signature, $credentialPublicKey)) { + throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE); + } + + $signatureCounter = $authenticatorObj->getSignCount(); + if ($signatureCounter !== 0) { + $this->_signatureCounter = $signatureCounter; + } + + // 17. If either of the signature counter value authData.signCount or + // previous signature count is nonzero, and if authData.signCount + // less than or equal to previous signature count, it's a signal + // that the authenticator may be cloned + if ($prevSignatureCnt !== null) { + if ($signatureCounter !== 0 || $prevSignatureCnt !== 0) { + if ($prevSignatureCnt >= $signatureCounter) { + throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER); + } + } + } + + return true; + } + + /** + * Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder + * https://fidoalliance.org/metadata/ + * @param string $certFolder Folder path to save the certificates in PEM format. + * @param bool $deleteCerts delete certificates in the target folder before adding the new ones. + * @return int number of cetificates + * @throws WebAuthnException + */ + public function queryFidoMetaDataService($certFolder, $deleteCerts=true) { + $url = 'https://mds.fidoalliance.org/'; + $raw = null; + if (\function_exists('curl_init')) { + $ch = \curl_init($url); + \curl_setopt($ch, CURLOPT_HEADER, false); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + \curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + \curl_setopt($ch, CURLOPT_USERAGENT, 'github.com/lbuchs/WebAuthn - A simple PHP WebAuthn server library'); + $raw = \curl_exec($ch); + \curl_close($ch); + } else { + $raw = \file_get_contents($url); + } + + $certFolder = \rtrim(\realpath($certFolder), '\\/'); + if (!is_dir($certFolder)) { + throw new WebAuthnException('Invalid folder path for query FIDO Alliance Metadata Service'); + } + + if (!\is_string($raw)) { + throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service'); + } + + $jwt = \explode('.', $raw); + if (\count($jwt) !== 3) { + throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service'); + } + + if ($deleteCerts) { + foreach (\scandir($certFolder) as $ca) { + if (\substr($ca, -4) === '.pem') { + if (\unlink($certFolder . DIRECTORY_SEPARATOR . $ca) === false) { + throw new WebAuthnException('Cannot delete certs in folder for FIDO Alliance Metadata Service'); + } + } + } + } + + list($header, $payload, $hash) = $jwt; + $payload = Binary\ByteBuffer::fromBase64Url($payload)->getJson(); + + $count = 0; + if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) { + foreach ($payload->entries as $entry) { + if (\is_object($entry) && \property_exists($entry, 'metadataStatement') && \is_object($entry->metadataStatement)) { + $description = $entry->metadataStatement->description ?? null; + $attestationRootCertificates = $entry->metadataStatement->attestationRootCertificates ?? null; + + if ($description && $attestationRootCertificates) { + + // create filename + $certFilename = \preg_replace('/[^a-z0-9]/i', '_', $description); + $certFilename = \trim(\preg_replace('/\_{2,}/i', '_', $certFilename),'_') . '.pem'; + $certFilename = \strtolower($certFilename); + + // add certificate + $certContent = $description . "\n"; + $certContent .= \str_repeat('-', \mb_strlen($description)) . "\n"; + + foreach ($attestationRootCertificates as $attestationRootCertificate) { + $attestationRootCertificate = \str_replace(["\n", "\r", ' '], '', \trim($attestationRootCertificate)); + $count++; + $certContent .= "\n-----BEGIN CERTIFICATE-----\n"; + $certContent .= \chunk_split($attestationRootCertificate, 64, "\n"); + $certContent .= "-----END CERTIFICATE-----\n"; + } + + if (\file_put_contents($certFolder . DIRECTORY_SEPARATOR . $certFilename, $certContent) === false) { + throw new WebAuthnException('unable to save certificate from FIDO Alliance Metadata Service'); + } + } + } + } + } + + return $count; + } + + // ----------------------------------------------- + // 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; + } + + /** + * check if the signature is valid. + * @param string $dataToVerify + * @param string $signature + * @param string $credentialPublicKey PEM format + * @return bool + */ + private function _verifySignature($dataToVerify, $signature, $credentialPublicKey) { + + // Use Sodium to verify EdDSA 25519 as its not yet supported by openssl + if (\function_exists('sodium_crypto_sign_verify_detached') && !\in_array('ed25519', \openssl_get_curve_names(), true)) { + $pkParts = []; + if (\preg_match('/BEGIN PUBLIC KEY\-+(?:\s|\n|\r)+([^\-]+)(?:\s|\n|\r)*\-+END PUBLIC KEY/i', $credentialPublicKey, $pkParts)) { + $rawPk = \base64_decode($pkParts[1]); + + // 30 = der sequence + // 2a = length 42 byte + // 30 = der sequence + // 05 = lenght 5 byte + // 06 = der OID + // 03 = OID length 3 byte + // 2b 65 70 = OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) + // 03 = der bit string + // 21 = length 33 byte + // 00 = null padding + // [...] = 32 byte x-curve + $okpPrefix = "\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00"; + + if ($rawPk && \strlen($rawPk) === 44 && \substr($rawPk,0, \strlen($okpPrefix)) === $okpPrefix) { + $publicKeyXCurve = \substr($rawPk, \strlen($okpPrefix)); + + return \sodium_crypto_sign_verify_detached($signature, $dataToVerify, $publicKeyXCurve); + } + } + } + + // verify with openSSL + $publicKey = \openssl_pkey_get_public($credentialPublicKey); + if ($publicKey === false) { + throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY); + } + + return \openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; + } +} diff --git a/server/app-code/system/insecure_zone/php/WebAuthnException.php b/server/app-code/system/insecure_zone/php/WebAuthnException.php new file mode 100644 index 0000000..f27eeec --- /dev/null +++ b/server/app-code/system/insecure_zone/php/WebAuthnException.php @@ -0,0 +1,28 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} + +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $_SESSION["username"]); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); + + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + } else if ($fn === 'getGetArgs') { + $ids = []; + + if ($requireResidentKey) { + if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { + throw new Exception('we do not have any registrations in session to check the registration'); + } + + } else { + // load registrations from session stored there by processCreate. + // normaly you have to load the credential Id's for a username + // from the database. + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->userId === $userId) { + $ids[] = $reg->credentialId; + } + } + } + + if (count($ids) === 0) { + throw new Exception('no registrations in session for userId ' . $userId); + } + } + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + } else if ($fn === 'processCreate') { + // Process create + $challenge = $_SESSION['challenge']; + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + + // Process create and store data in the database + $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); + + // add user infos + $data->userId = $userId; + $data->userName = $userName; + $data->userDisplayName = $userDisplayName; + + // Store registration data in the database + $stmt = $conn->prepare("UPDATE users set user_hex_id = ?, credential_id = ?, public_key = ?, counter = ? WHERE username = ?"); + //$stmt = $conn->prepare("INSERT INTO users (user_hex_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)"); + //var_dump($data); + $stmt->execute([$userId, $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]); + + $msg = 'registration success.'; + $return = new stdClass(); + $return->success = true; + $return->msg = $msg; + header('Content-Type: application/json'); + print(json_encode($return)); + log_action("PASSWD::PASSKEY::ADD","User ".$_SESSION["username"]." added a passkey.",$_SESSION["id"]); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} +?> \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/create_admin_backend.php b/server/app-code/system/insecure_zone/php/create_admin_backend.php new file mode 100644 index 0000000..269374f --- /dev/null +++ b/server/app-code/system/insecure_zone/php/create_admin_backend.php @@ -0,0 +1,187 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $userName); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); + + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + } else if ($fn === 'getGetArgs') { + $ids = []; + + if ($requireResidentKey) { + if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { + throw new Exception('we do not have any registrations in session to check the registration'); + } + + } else { + // load registrations from session stored there by processCreate. + // normaly you have to load the credential Id's for a username + // from the database. + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->userId === $userId) { + $ids[] = $reg->credentialId; + } + } + } + + if (count($ids) === 0) { + throw new Exception('no registrations in session for userId ' . $userId); + } + } + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + } else if ($fn === 'processCreate') { + // Process create + $challenge = $_SESSION['challenge']; + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + + // Process create and store data in the database + $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); + + // add user infos + $data->userId = $userId; + $data->userName = $userName; + $data->userDisplayName = $userDisplayName; + + // Store registration data in the database + $stmt = $conn->prepare("UPDATE users set user_hex_id = ?, credential_id = ?, public_key = ?, counter = ? WHERE username = ?"); + //$stmt = $conn->prepare("INSERT INTO users (user_hex_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)"); + //var_dump($data); + $stmt->execute([$userId, $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]); + + $msg = 'registration success.'; + $return = new stdClass(); + $return->success = true; + $return->msg = $msg; + header('Content-Type: application/json'); + print(json_encode($return)); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} +?> \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/login.php b/server/app-code/system/insecure_zone/php/login.php new file mode 100644 index 0000000..3d59ed7 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/login.php @@ -0,0 +1,366 @@ + + + + + + + + Cyberhex login page + + + + +
+
+
+
+
+
+

Login to Cyberhex

+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
Or
+
+
+ +
+
+
+

+ + + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + + // Check if the user exists and verify the password + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + if($row["allow_pw_login"]==1){ + if (password_verify($password, $row['password'])) { + $_SESSION["username"]=htmlspecialchars($username); + $_SESSION["login"]=true; + $_SESSION["perms"]=$row["perms"]; + $_SESSION["email"]=$row["email"]; + $_SESSION["id"]=$row["id"]; + $_SESSION["telegram_id"]=$row["telegram_id"]; + $_SESSION["allow_pw_login"]=$row["allow_pw_login"]; + $_SESSION["send_login_message"]=$row["send_login_message"]; + $_SESSION["use_2fa"]=$row["use_2fa"]; + if($_SESSION["use_2fa"]=="1"){ + unset($_SESSION["login"]); //set the login state to false + $_SESSION["2fa_auth"]=true; + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + //send the user to 2fa auth page + echo ''; + }else{ + log_action("LOGIN::SUCCESS","User ".$_SESSION["username"]." logged in with password.",$_SESSION["id"]); + if($_SESSION["send_login_message"]=="1"){ + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[LOGIN WARNING]\nHello $username\nSomebody has logged into Cyberhex with your account.\nIf this was you, you can ignore this message. Else please take steps to secure your account!\nIP: $ip\n",$username); + } + echo ''; + } + exit(); + } else { + log_action("LOGIN::FAILURE","User ".$username." entered wrong password.",1); + echo ''; + } + } + else{ + echo ''; + } + } else { + log_action("LOGIN::FAILURE","User ".$username." entered unknown username.",1); + echo ''; + } + + + // Close the connection + $stmt->close(); + $conn->close(); + } + ?> + +
+
+
+
+
+
+ + diff --git a/server/app-code/system/insecure_zone/php/login_backend.php b/server/app-code/system/insecure_zone/php/login_backend.php new file mode 100644 index 0000000..8fe0a0f --- /dev/null +++ b/server/app-code/system/insecure_zone/php/login_backend.php @@ -0,0 +1,220 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} +include "../../../api/php/notifications/sendmessage.php"; //to send user notification on login +include "../../../api/php/log/add_server_entry.php"; //to log things +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $userName); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + // Get create arguments + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification); + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // Save challenge to session or somewhere else if needed + } else if ($fn === 'getGetArgs') { + $ids = []; + + //get registrations form user table + //put credential id into session where userid = $userId + + $stmt = $conn->prepare("SELECT credential_id FROM users WHERE user_hex_id = ?"); + $stmt->bind_param("s", $userId); + $stmt->execute(); + $registration = $stmt->get_result(); + $row = $registration->fetch_assoc(); + + + if ($registration->num_rows <= 0) { + throw new Exception('User does not exist'); + } + + $_SESSION["registrations"]["credentialId"]=$row["credential_id"]; + + $ids[]=$row["credential_id"]; + $_SESSION["registrations"]["userId"]=$userId; + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + }else if ($fn === 'processGet') { + // Process get + // Retrieve registration data from the database based on credential ID + $id = base64_decode($post->id); + $stmt = $conn->prepare("SELECT * FROM users WHERE credential_id = ?"); + $stmt->bind_param("s", $_SESSION["registrations"]["credentialId"]); + $stmt->execute(); + $registration = $stmt->get_result(); + $row = $registration->fetch_assoc(); + + if (!$registration) { + throw new Exception('Public Key for credential ID not found!'); + } + + $clientDataJSON = base64_decode($post->clientDataJSON); + $authenticatorData = base64_decode($post->authenticatorData); + $signature = base64_decode($post->signature); + $userHandle = base64_decode($post->userHandle); + $challenge = $_SESSION['challenge'] ?? ''; + $credentialPublicKey = $row['public_key']; + + // Process the get request + $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required'); + + // Authentication success + //set sessionso user is authenticated + $_SESSION["username"]=htmlspecialchars($row["username"]); + $_SESSION["login"]=true; + $_SESSION["perms"]=$row["perms"]; + $_SESSION["id"]=$row["id"]; + $_SESSION["email"]=$row["email"]; + $_SESSION["telegram_id"]=$row["telegram_id"]; + $_SESSION["allow_pw_login"]=$row["allow_pw_login"]; + $_SESSION["send_login_message"]=$row["send_login_message"]; + $_SESSION["use_2fa"]=$row["use_2fa"]; + + $return = new stdClass(); + $return->success = true; + + if($_SESSION["use_2fa"]=="1"){ + unset($_SESSION["login"]); //set the login state to false + $_SESSION["2fa_auth"]=true; + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + //send the user to 2fa auth page + $return->msg="send_to_2fa"; + }else{ + log_action("LOGIN::SUCCESS","User ".$_SESSION["username"]." logged in with passkey.",$_SESSION["id"]); + if($_SESSION["send_login_message"]=="1"){ + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[LOGIN WARNING]\nHello $username\nSomebody has logged into Cyberhex with your account.\nIf this was you, you can ignore this message. Else please take steps to secure your account!\nIP: $ip\n",$username); + } + } + + header('Content-Type: application/json'); + print(json_encode($return)); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} + +?> diff --git a/server/app-code/system/insecure_zone/php/logout.php b/server/app-code/system/insecure_zone/php/logout.php new file mode 100644 index 0000000..2cda7df --- /dev/null +++ b/server/app-code/system/insecure_zone/php/logout.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/no_access.php b/server/app-code/system/insecure_zone/php/no_access.php new file mode 100644 index 0000000..5c3ea30 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/no_access.php @@ -0,0 +1,28 @@ + + + + + + + Change Password + + + +
+
+
+
+
+

No Access

+
+
+ +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/add_user.php b/server/app-code/system/secure_zone/php/add_user.php new file mode 100644 index 0000000..ee9f169 --- /dev/null +++ b/server/app-code/system/secure_zone/php/add_user.php @@ -0,0 +1,231 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Add a user

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionAllow/Deny
1Add user (Warning!)
2Delete/list/manage user (Warning!)
3View log
4Delete log
5Server Settings
6Client settings
7Database settings
8Add clients
9Delete/list clients
10View Incidents
11Manage Incidents
+ + +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + $stmt->close(); + $conn->close(); + + + // Check if the user exists and verify the password + if ($result->num_rows > 0) { + echo ''; + + }else{ + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES (?, ?, ?, ?,1,0,0)"); + $stmt->bind_param("ssss", $email, $username, $hash, $permissions); + + $email=htmlspecialchars($_POST["email"]); + $username=htmlspecialchars($_POST["username"]); + $password=$_POST["password"]; + $permissions=get_perm_str(); + $hash=password_hash($password, PASSWORD_BCRYPT); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + echo ''; + + } + }elseif($block==1){ + echo ''; + } + ?> + +
+
+
+ + +
+
+ + diff --git a/server/app-code/system/secure_zone/php/index.php b/server/app-code/system/secure_zone/php/index.php new file mode 100644 index 0000000..d043041 --- /dev/null +++ b/server/app-code/system/secure_zone/php/index.php @@ -0,0 +1,137 @@ + + + + + + + + + Cyberhex (<?php echo(str_replace("_"," ",explode(".",$page))[0]); ?>) + + + + + +
+
+ + +
+

Home

+ +

User

+ + Log

"); + ?> + + Cyberhex settings

"); + ?> + + Clients

"); + ?> + + + Incidents

"); + ?> + + +
+ + +
+ + + +
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/manage_user.php b/server/app-code/system/secure_zone/php/manage_user.php new file mode 100644 index 0000000..4663da3 --- /dev/null +++ b/server/app-code/system/secure_zone/php/manage_user.php @@ -0,0 +1,272 @@ + + + + + + + + Change Password + + +connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql="SELECT * FROM users WHERE id=?"; + $stmt = $conn->prepare($sql); + $m_userid=htmlspecialchars($_GET["userid"]); + $stmt->bind_param("i", $m_userid); + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $stmt->close(); + $m_username=$row["username"]; + $m_email=$row["email"]; + $m_permissions=$row["perms"]; + $conn->close(); +?> +
+
+
+
+
+

Manage a user

+
+
+
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + +
#DescriptionAllow/Deny
1Add user (Warning!)
2Delete/list/manage user (Warning!)
3View log
4Delete log
5Server Settings
6Client settings
7Database settings
8Add clients
9Delete/list clients
10View Incidents
11Manage Incidents
+ + +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + + $stmt = $conn->prepare("UPDATE users set email=?, username=?,perms=? WHERE id=?"); + $stmt->bind_param("sssi", $m_email, $m_username, $m_permissions,$m_userid); + + $m_email=htmlspecialchars($_POST["email"]); + $m_username=htmlspecialchars($_POST["username"]); + $m_permissions=get_perm_str(); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + + echo(""); + }elseif($block==1){ + echo ''; + } + ?> + +
+
+
+ + +
+
+ + diff --git a/server/app-code/system/secure_zone/php/passwd.php b/server/app-code/system/secure_zone/php/passwd.php new file mode 100644 index 0000000..a998856 --- /dev/null +++ b/server/app-code/system/secure_zone/php/passwd.php @@ -0,0 +1,333 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Change Password ()

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Or +
+ +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + $stmt->close(); + $conn->close(); + + + // Check if the user exists and verify the password + if($new_password1===$new_password2){ + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + if (password_verify($password, $row['password'])) { + //password correct update + // Create connection + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + + // Check connection + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $stmt = $conn->prepare("UPDATE users set password = ? where username = ?"); + $stmt->bind_param("ss", $hash, $username); + $stmt->execute(); + $stmt->close(); + $conn->close(); + + echo '
'; + + } else { + + echo ''; + } + } else { + + echo ''; + } + }else{ + echo ''; + } + + } + ?> + +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/perms_functions.php b/server/app-code/system/secure_zone/php/perms_functions.php new file mode 100644 index 0000000..2fbbb96 --- /dev/null +++ b/server/app-code/system/secure_zone/php/perms_functions.php @@ -0,0 +1,71 @@ + \ No newline at end of file diff --git a/server/app-code/system/secure_zone/php/profile.php b/server/app-code/system/secure_zone/php/profile.php new file mode 100644 index 0000000..de5bb34 --- /dev/null +++ b/server/app-code/system/secure_zone/php/profile.php @@ -0,0 +1,167 @@ + +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $user_hex_id=bin2hex($username_new); + $stmt = $conn->prepare("UPDATE users set email = ?, username = ?, telegram_id = ?, allow_pw_login = ?, user_hex_id = ?, send_login_message = ?, use_2fa = ? where username = ?"); + $stmt->bind_param("sssisiis", $email, $username_new,$telegram_id, $pw_login,$user_hex_id, $send_login_message, $use_2fa, $username); + + $email=htmlspecialchars($_POST["email"]); + $username_new=htmlspecialchars($_POST["username"]); + $telegram_id=htmlspecialchars($_POST["telegram_id"]); + $stmt->execute(); + $stmt->close(); + $conn->close(); + $username=$username_new; + $_SESSION["username"]=$username; + $_SESSION["email"]=$email; + $_SESSION["telegram_id"]=$telegram_id; + $_SESSION["allow_pw_login"]=$pw_login; + $_SESSION["send_login_message"]=$send_login_message; + $_SESSION["use_2fa"]=$use_2fa; +} + +?> + + + + + + + Profile + + +
+
+
+
+
+

Your Profile ()

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+ +
+ '; + } + ?> +
+
+
+ +
+
+ + + diff --git a/server/app-code/system/secure_zone/php/user_list.php b/server/app-code/system/secure_zone/php/user_list.php new file mode 100644 index 0000000..59602ed --- /dev/null +++ b/server/app-code/system/secure_zone/php/user_list.php @@ -0,0 +1,127 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

User list

+
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + //delete user if requested + if(isset($_GET["delete"])){ + $userid=htmlspecialchars($_GET["delete"]); + $sql = "DELETE FROM users WHERE id = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("i", $userid); + // Execute the statement + $stmt->execute(); + $stmt->close(); + } + + + //get count of users + + $sql = "SELECT count(*) AS user_count FROM users"; + $stmt = $conn->prepare($sql); + // Execute the statement + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $num_of_users=$row["user_count"]; + $stmt->close(); + + //now list of all the users => userid, username, email, perms, delete + // Create a connection + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE); + + // Check the connection + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $last_id=-1; + //create the table header + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + while($num_of_users!=0){ + $sql = "SELECT * FROM users where id > $last_id"; + $stmt = $conn->prepare($sql); + // Execute the statement + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $last_id=$row["id"]; + $username=$row["username"]; + $email=$row["email"]; + $perms=$row["perms"]; + if($last_id!=1){ //number 1 is the unauthenticated user + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + } + $stmt->close(); + $num_of_users--; + } + echo(''); + echo('
UseridUsernameEmailPermissionsEdit UserDelete user
'.$last_id.''.$username.''.$email.''.$perms.'managedelete
'); + $conn->close(); + ?> +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/welcome.php b/server/app-code/system/secure_zone/php/welcome.php new file mode 100644 index 0000000..ba84c41 --- /dev/null +++ b/server/app-code/system/secure_zone/php/welcome.php @@ -0,0 +1,39 @@ + + + + + + + + Cyberhex + + +

+
+
+
+
+
+

Dashboard

+
+
+
+
+
+
+
+ + diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..abec619 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,42 @@ +version: '3.3' + +services: + app-db: + image: yobasystems/alpine-mariadb:latest + container_name: app-db + environment: + MYSQL_ROOT_PASSWORD: 1234 + networks: + app-network: + ipv4_address: 192.168.178.2 + volumes: + - app-db-storage:/var/lib/mysql + + app-srv: + build: + context: . + dockerfile: srv_dockerfile + container_name: app-srv + networks: + app-network: + ipv4_address: 192.168.178.3 + ports: + - "8088:80" + - "4438:443" + depends_on: + - app-db + volumes: + - ./app-code:/var/www/html + - ./apache-conf:/etc/apache2/sites-available + - ./certs:/etc/apache2/certs +networks: + app-network: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.178.0/24 + +volumes: + app-db-storage: + external: true diff --git a/server/install.sh b/server/install.sh new file mode 100644 index 0000000..925bb39 --- /dev/null +++ b/server/install.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker volume create app-db-storage diff --git a/server/install.txt b/server/install.txt new file mode 100644 index 0000000..6041882 --- /dev/null +++ b/server/install.txt @@ -0,0 +1,5 @@ +1: download the file docker-compose.yml and the folder cyberhex-code +2: create the cyberhex-db-storage volume +3: request some ssl certs via certbot form letsencrypt and put them into certs/ +sudo certbot certonly --manual --preferred-challenges=http --http-01-port=8080 +3: use docker-compose up to start the whole thing diff --git a/server/srv_dockerfile b/server/srv_dockerfile new file mode 100644 index 0000000..527c062 --- /dev/null +++ b/server/srv_dockerfile @@ -0,0 +1,16 @@ +# Extend the official PHP image +FROM php:apache + +# Install the mysqli extension +RUN docker-php-ext-install mysqli +RUN a2enmod ssl +RUN service apache2 restart +COPY ./cyberhex-code /var/www/html +RUN mkdir -p /var/www/html/install/ +RUN mkdir -p /var/www/html/database_srv +RUN mkdir -p /var/www/html/export +RUN mkdir -p /var/www/html/import +RUN chown -R www-data:www-data /var/www/html/export/ +RUN chown -R www-data:www-data /var/www/html/import/ +RUN chown -R www-data:www-data /var/www/html/install/ +RUN chown -R www-data:www-data /var/www/html/database_srv/ \ No newline at end of file