Skip to content

Commit

Permalink
EdDSA
Browse files Browse the repository at this point in the history
Verify EdDSA using Sodium
  • Loading branch information
lbuchs committed Oct 23, 2023
1 parent 7d3ea0d commit 1cc44fb
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions _test/client.html
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,11 @@ <h1 style="margin: 40px 10px 2px 0;">lbuchs/WebAuthn</h1>
</div>
<div>
<input type="checkbox" id="type_hybrid" name="type_hybrid" checked>
<label for="type_hybrid">hybrid <i style="font-size: 0.8em;">passkeys, ...</i></label>
<label for="type_hybrid">hybrid <i style="font-size: 0.8em;">Passkeys via mobile device, ...</i></label>
</div>
<div>
<input type="checkbox" id="type_int" name="type_int" checked>
<label for="type_int">internal <i style="font-size: 0.8em;">Windows Hello, Android SafetyNet, Apple, ...</i></label>
<label for="type_int">internal <i style="font-size: 0.8em;">Passkeys on the device, Windows Hello, Android SafetyNet, Apple, ...</i></label>
</div>

<div>&nbsp;</div>
Expand Down
4 changes: 2 additions & 2 deletions _test/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
// ------------------------------------

if ($fn === 'getCreateArgs') {
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 20, $requireResidentKey, $userVerification, $crossPlatformAttachment);
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);

header('Content-Type: application/json');
print(json_encode($createArgs));
Expand Down Expand Up @@ -183,7 +183,7 @@
}
}

$getArgs = $WebAuthn->getGetArgs($ids, 20, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);

header('Content-Type: application/json');
print(json_encode($getArgs));
Expand Down
64 changes: 61 additions & 3 deletions src/Attestation/AuthenticatorData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -115,10 +122,15 @@ public function getCredentialId() {
* @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);
}

Expand All @@ -134,9 +146,12 @@ public function getPublicKeyPem() {
* @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;
Expand Down Expand Up @@ -192,6 +207,19 @@ private function _getEc2Der() {
);
}

/**
* 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
Expand Down Expand Up @@ -283,11 +311,41 @@ private function _readCredentialPublicKey($binary, $offset, &$endOffset) {
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
Expand Down
89 changes: 70 additions & 19 deletions src/WebAuthn.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlE
$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()))) {
Expand All @@ -73,7 +73,7 @@ public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlE
*/
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');
Expand Down Expand Up @@ -122,7 +122,7 @@ public function getChallenge() {
* @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();
Expand Down Expand Up @@ -166,12 +166,23 @@ public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20,
$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';
Expand All @@ -194,7 +205,7 @@ public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20,
$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) {
Expand Down Expand Up @@ -228,7 +239,7 @@ public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20,
* 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)) {
Expand All @@ -247,12 +258,12 @@ public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true,
$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';
Expand Down Expand Up @@ -468,12 +479,7 @@ public function processGet($clientDataJSON, $authenticatorData, $signature, $cre
$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);
}

Expand Down Expand Up @@ -623,4 +629,49 @@ private function _createChallenge($length = 32) {
}
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;
}
}

0 comments on commit 1cc44fb

Please sign in to comment.