Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?phpnamespace lbuchs\WebAuthn\Attestation;use lbuchs\WebAuthn\WebAuthnException;use lbuchs\WebAuthn\CBOR\CborDecoder;use lbuchs\WebAuthn\Binary\ByteBuffer;/*** @author Lukas Buchs* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT*/class AuthenticatorData {protected $_binary;protected $_rpIdHash;protected $_flags;protected $_signCount;protected $_attestedCredentialData;protected $_extensionData;// Cose encoded keysprivate static $_COSE_KTY = 1;private static $_COSE_ALG = 3;// Cose curveprivate static $_COSE_CRV = -1;private static $_COSE_X = -2;private static $_COSE_Y = -3;// Cose RSA PS256private static $_COSE_N = -1;private static $_COSE_E = -2;// EC2 key typeprivate static $_EC2_TYPE = 2;private static $_EC2_ES256 = -7;private static $_EC2_P256 = 1;// RSA key typeprivate static $_RSA_TYPE = 3;private static $_RSA_RS256 = -257;// OKP key typeprivate static $_OKP_TYPE = 1;private static $_OKP_ED25519 = 6;private static $_OKP_EDDSA = -8;/*** Parsing the authenticatorData binary.* @param string $binary* @throws WebAuthnException*/public function __construct($binary) {if (!\is_string($binary) || \strlen($binary) < 37) {throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA);}$this->_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-dataif ($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);// Validationif ($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);// Validationif ($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);// Validationif ($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 bytesfor ($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 numberif ((\ord($bytes[0]) & 0x80) !== 0) {$bytes = "\x00" . $bytes;}return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes;}}