ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç Format/AndroidKey.php000064400000006715152267342220010554 0ustar00_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; } } Format/FormatBase.php000064400000013557152267342220010550 0ustar00_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 string|null */ 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; } } Format/Tpm.php000064400000014725152267342220007263 0ustar00_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); } } Format/None.php000064400000001666152267342220007422 0ustar00_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; } } Format/Packed.php000064400000011265152267342220007706 0ustar00_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; } } Format/Apple.php000064400000012073152267342220007556 0ustar00_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; } } Format/AndroidSafetyNet.php000064400000013335152267342220011722 0ustar00_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)); } } AttestationObject.php000064400000014120152267342220010706 0ustar00_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(); } } AuthenticatorData.php000064400000040412152267342220010667 0ustar00_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; } /** * returns true if the backup is eligible * @return boolean */ public function getIsBackupEligible() { return $this->_flags->isBackupEligible; } /** * returns true if the current credential is backed up * @return boolean */ public function getIsBackup() { return $this->_flags->isBackup; } // ----------------------------------------------- // 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->isBackupEligible = $flags->bit_3; $flags->isBackup = $flags->bit_4; $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; } }