mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 00:42:54 +02:00
MDL-80638 lib: Update WebAuthn to 2.1.1
This commit is contained in:
parent
b621a7e4b3
commit
428221829d
@ -119,6 +119,7 @@ Availability of built-in passkeys that automatically synchronize to all of a use
|
||||
## Requirements
|
||||
* PHP >= 8.0 with [OpenSSL](http://php.net/manual/en/book.openssl.php) and [Multibyte String](https://www.php.net/manual/en/book.mbstring.php)
|
||||
* Browser with [WebAuthn support](https://caniuse.com/webauthn) (Firefox 60+, Chrome 67+, Edge 18+, Safari 13+)
|
||||
* PHP [Sodium](https://www.php.net/manual/en/book.sodium.php) (or [Sodium Compat](https://github.com/paragonie/sodium_compat) ) for [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) support
|
||||
|
||||
## Infos about WebAuthn
|
||||
* [Wikipedia](https://en.wikipedia.org/wiki/WebAuthn)
|
||||
|
@ -23,7 +23,7 @@ class AuthenticatorData {
|
||||
private static $_COSE_KTY = 1;
|
||||
private static $_COSE_ALG = 3;
|
||||
|
||||
// Cose EC2 ES256 P-256 curve
|
||||
// Cose curve
|
||||
private static $_COSE_CRV = -1;
|
||||
private static $_COSE_X = -2;
|
||||
private static $_COSE_Y = -3;
|
||||
@ -32,13 +32,20 @@ class AuthenticatorData {
|
||||
private static $_COSE_N = -1;
|
||||
private static $_COSE_E = -2;
|
||||
|
||||
// EC2 key type
|
||||
private static $_EC2_TYPE = 2;
|
||||
private static $_EC2_ES256 = -7;
|
||||
private static $_EC2_P256 = 1;
|
||||
|
||||
// RSA key type
|
||||
private static $_RSA_TYPE = 3;
|
||||
private static $_RSA_RS256 = -257;
|
||||
|
||||
// OKP key type
|
||||
private static $_OKP_TYPE = 1;
|
||||
private static $_OKP_ED25519 = 6;
|
||||
private static $_OKP_EDDSA = -8;
|
||||
|
||||
/**
|
||||
* Parsing the authenticatorData binary.
|
||||
* @param string $binary
|
||||
@ -115,10 +122,15 @@ class AuthenticatorData {
|
||||
* @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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -134,9 +146,12 @@ class AuthenticatorData {
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getPublicKeyU2F() {
|
||||
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||
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;
|
||||
@ -192,6 +207,19 @@ class AuthenticatorData {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -283,11 +311,41 @@ class AuthenticatorData {
|
||||
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
|
||||
|
@ -123,7 +123,7 @@ abstract class FormatBase {
|
||||
}
|
||||
|
||||
if ($content) {
|
||||
$this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem';
|
||||
$this->_x5c_tempFile = \tempnam(\sys_get_temp_dir(), 'x5c_');
|
||||
if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
|
||||
return $this->_x5c_tempFile;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class WebAuthn {
|
||||
$supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm');
|
||||
|
||||
if (!\function_exists('\openssl_open')) {
|
||||
throw new WebAuthnException('OpenSSL-Module not installed');;
|
||||
throw new WebAuthnException('OpenSSL-Module not installed');
|
||||
}
|
||||
|
||||
if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) {
|
||||
@ -73,7 +73,7 @@ class WebAuthn {
|
||||
*/
|
||||
public function addRootCertificates($path, $certFileExtensions=null) {
|
||||
if (!\is_array($this->_caFiles)) {
|
||||
$this->_caFiles = array();
|
||||
$this->_caFiles = [];
|
||||
}
|
||||
if ($certFileExtensions === null) {
|
||||
$certFileExtensions = array('pem', 'crt', 'cer', 'der');
|
||||
@ -122,7 +122,7 @@ class WebAuthn {
|
||||
* @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=array()) {
|
||||
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $crossPlatformAttachment=null, $excludeCredentialIds=[]) {
|
||||
|
||||
$args = new \stdClass();
|
||||
$args->publicKey = new \stdClass();
|
||||
@ -166,12 +166,23 @@ class WebAuthn {
|
||||
$args->publicKey->user->displayName = $userDisplayName;
|
||||
|
||||
// supported algorithms
|
||||
$args->publicKey->pubKeyCredParams = array();
|
||||
$tmp = new \stdClass();
|
||||
$tmp->type = 'public-key';
|
||||
$tmp->alg = -7; // ES256
|
||||
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||
unset ($tmp);
|
||||
$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';
|
||||
@ -194,7 +205,7 @@ class WebAuthn {
|
||||
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||
|
||||
//prevent re-registration by specifying existing credentials
|
||||
$args->publicKey->excludeCredentials = array();
|
||||
$args->publicKey->excludeCredentials = [];
|
||||
|
||||
if (is_array($excludeCredentialIds)) {
|
||||
foreach ($excludeCredentialIds as $id) {
|
||||
@ -228,7 +239,7 @@ class WebAuthn {
|
||||
* string 'required' 'preferred' 'discouraged'
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowHybrid=true, $allowInternal=true, $requireUserVerification=false) {
|
||||
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)) {
|
||||
@ -247,12 +258,12 @@ class WebAuthn {
|
||||
$args->publicKey->rpId = $this->_rpId;
|
||||
|
||||
if (\is_array($credentialIds) && \count($credentialIds) > 0) {
|
||||
$args->publicKey->allowCredentials = array();
|
||||
$args->publicKey->allowCredentials = [];
|
||||
|
||||
foreach ($credentialIds as $id) {
|
||||
$tmp = new \stdClass();
|
||||
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||
$tmp->transports = array();
|
||||
$tmp->transports = [];
|
||||
|
||||
if ($allowUsb) {
|
||||
$tmp->transports[] = 'usb';
|
||||
@ -468,12 +479,7 @@ class WebAuthn {
|
||||
$dataToVerify .= $authenticatorData;
|
||||
$dataToVerify .= $clientDataHash;
|
||||
|
||||
$publicKey = \openssl_pkey_get_public($credentialPublicKey);
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||
if (!$this->_verifySignature($dataToVerify, $signature, $credentialPublicKey)) {
|
||||
throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
@ -623,4 +629,49 @@ class WebAuthn {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user