1 <?php 2 3 namespace lbuchs\WebAuthn\Attestation; 4 use lbuchs\WebAuthn\WebAuthnException; 5 use lbuchs\WebAuthn\CBOR\CborDecoder; 6 use lbuchs\WebAuthn\Binary\ByteBuffer; 7 8 /** 9 * @author Lukas Buchs 10 * @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT 11 */ 12 class AuthenticatorData { 13 protected $_binary; 14 protected $_rpIdHash; 15 protected $_flags; 16 protected $_signCount; 17 protected $_attestedCredentialData; 18 protected $_extensionData; 19 20 21 22 // Cose encoded keys 23 private static $_COSE_KTY = 1; 24 private static $_COSE_ALG = 3; 25 26 // Cose EC2 ES256 P-256 curve 27 private static $_COSE_CRV = -1; 28 private static $_COSE_X = -2; 29 private static $_COSE_Y = -3; 30 31 // Cose RSA PS256 32 private static $_COSE_N = -1; 33 private static $_COSE_E = -2; 34 35 private static $_EC2_TYPE = 2; 36 private static $_EC2_ES256 = -7; 37 private static $_EC2_P256 = 1; 38 39 private static $_RSA_TYPE = 3; 40 private static $_RSA_RS256 = -257; 41 42 /** 43 * Parsing the authenticatorData binary. 44 * @param string $binary 45 * @throws WebAuthnException 46 */ 47 public function __construct($binary) { 48 if (!\is_string($binary) || \strlen($binary) < 37) { 49 throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA); 50 } 51 $this->_binary = $binary; 52 53 // Read infos from binary 54 // https://www.w3.org/TR/webauthn/#sec-authenticator-data 55 56 // RP ID 57 $this->_rpIdHash = \substr($binary, 0, 32); 58 59 // flags (1 byte) 60 $flags = \unpack('Cflags', \substr($binary, 32, 1))['flags']; 61 $this->_flags = $this->_readFlags($flags); 62 63 // signature counter: 32-bit unsigned big-endian integer. 64 $this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount']; 65 66 $offset = 37; 67 // https://www.w3.org/TR/webauthn/#sec-attested-credential-data 68 if ($this->_flags->attestedDataIncluded) { 69 $this->_attestedCredentialData = $this->_readAttestData($binary, $offset); 70 } 71 72 if ($this->_flags->extensionDataIncluded) { 73 $this->_readExtensionData(\substr($binary, $offset)); 74 } 75 } 76 77 /** 78 * Authenticator Attestation Globally Unique Identifier, a unique number 79 * that identifies the model of the authenticator (not the specific instance 80 * of the authenticator) 81 * The aaguid may be 0 if the user is using a old u2f device and/or if 82 * the browser is using the fido-u2f format. 83 * @return string 84 * @throws WebAuthnException 85 */ 86 public function getAAGUID() { 87 if (!($this->_attestedCredentialData instanceof \stdClass)) { 88 throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); 89 } 90 return $this->_attestedCredentialData->aaguid; 91 } 92 93 /** 94 * returns the authenticatorData as binary 95 * @return string 96 */ 97 public function getBinary() { 98 return $this->_binary; 99 } 100 101 /** 102 * returns the credentialId 103 * @return string 104 * @throws WebAuthnException 105 */ 106 public function getCredentialId() { 107 if (!($this->_attestedCredentialData instanceof \stdClass)) { 108 throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA); 109 } 110 return $this->_attestedCredentialData->credentialId; 111 } 112 113 /** 114 * returns the public key in PEM format 115 * @return string 116 */ 117 public function getPublicKeyPem() { 118 $der = null; 119 switch ($this->_attestedCredentialData->credentialPublicKey->kty) { 120 case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break; 121 case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break; 122 default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA); 123 } 124 125 $pem = '-----BEGIN PUBLIC KEY-----' . "\n"; 126 $pem .= \chunk_split(\base64_encode($der), 64, "\n"); 127 $pem .= '-----END PUBLIC KEY-----' . "\n"; 128 return $pem; 129 } 130 131 /** 132 * returns the public key in U2F format 133 * @return string 134 * @throws WebAuthnException 135 */ 136 public function getPublicKeyU2F() { 137 if (!($this->_attestedCredentialData instanceof \stdClass)) { 138 throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); 139 } 140 return "\x04" . // ECC uncompressed 141 $this->_attestedCredentialData->credentialPublicKey->x . 142 $this->_attestedCredentialData->credentialPublicKey->y; 143 } 144 145 /** 146 * returns the SHA256 hash of the relying party id (=hostname) 147 * @return string 148 */ 149 public function getRpIdHash() { 150 return $this->_rpIdHash; 151 } 152 153 /** 154 * returns the sign counter 155 * @return int 156 */ 157 public function getSignCount() { 158 return $this->_signCount; 159 } 160 161 /** 162 * returns true if the user is present 163 * @return boolean 164 */ 165 public function getUserPresent() { 166 return $this->_flags->userPresent; 167 } 168 169 /** 170 * returns true if the user is verified 171 * @return boolean 172 */ 173 public function getUserVerified() { 174 return $this->_flags->userVerified; 175 } 176 177 // ----------------------------------------------- 178 // PRIVATE 179 // ----------------------------------------------- 180 181 /** 182 * Returns DER encoded EC2 key 183 * @return string 184 */ 185 private function _getEc2Der() { 186 return $this->_der_sequence( 187 $this->_der_sequence( 188 $this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey 189 $this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1 190 ) . 191 $this->_der_bitString($this->getPublicKeyU2F()) 192 ); 193 } 194 195 /** 196 * Returns DER encoded RSA key 197 * @return string 198 */ 199 private function _getRsaDer() { 200 return $this->_der_sequence( 201 $this->_der_sequence( 202 $this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption 203 $this->_der_nullValue() 204 ) . 205 $this->_der_bitString( 206 $this->_der_sequence( 207 $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) . 208 $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e) 209 ) 210 ) 211 ); 212 } 213 214 /** 215 * reads the flags from flag byte 216 * @param string $binFlag 217 * @return \stdClass 218 */ 219 private function _readFlags($binFlag) { 220 $flags = new \stdClass(); 221 222 $flags->bit_0 = !!($binFlag & 1); 223 $flags->bit_1 = !!($binFlag & 2); 224 $flags->bit_2 = !!($binFlag & 4); 225 $flags->bit_3 = !!($binFlag & 8); 226 $flags->bit_4 = !!($binFlag & 16); 227 $flags->bit_5 = !!($binFlag & 32); 228 $flags->bit_6 = !!($binFlag & 64); 229 $flags->bit_7 = !!($binFlag & 128); 230 231 // named flags 232 $flags->userPresent = $flags->bit_0; 233 $flags->userVerified = $flags->bit_2; 234 $flags->attestedDataIncluded = $flags->bit_6; 235 $flags->extensionDataIncluded = $flags->bit_7; 236 return $flags; 237 } 238 239 /** 240 * read attested data 241 * @param string $binary 242 * @param int $endOffset 243 * @return \stdClass 244 * @throws WebAuthnException 245 */ 246 private function _readAttestData($binary, &$endOffset) { 247 $attestedCData = new \stdClass(); 248 if (\strlen($binary) <= 55) { 249 throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA); 250 } 251 252 // The AAGUID of the authenticator 253 $attestedCData->aaguid = \substr($binary, 37, 16); 254 255 //Byte length L of Credential ID, 16-bit unsigned big-endian integer. 256 $length = \unpack('nlength', \substr($binary, 53, 2))['length']; 257 $attestedCData->credentialId = \substr($binary, 55, $length); 258 259 // set end offset 260 $endOffset = 55 + $length; 261 262 // extract public key 263 $attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset); 264 265 return $attestedCData; 266 } 267 268 /** 269 * reads COSE key-encoded elliptic curve public key in EC2 format 270 * @param string $binary 271 * @param int $endOffset 272 * @return \stdClass 273 * @throws WebAuthnException 274 */ 275 private function _readCredentialPublicKey($binary, $offset, &$endOffset) { 276 $enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset); 277 278 // COSE key-encoded elliptic curve public key in EC2 format 279 $credPKey = new \stdClass(); 280 $credPKey->kty = $enc[self::$_COSE_KTY]; 281 $credPKey->alg = $enc[self::$_COSE_ALG]; 282 283 switch ($credPKey->alg) { 284 case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break; 285 case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break; 286 } 287 288 return $credPKey; 289 } 290 291 /** 292 * extract ES256 informations from cose 293 * @param \stdClass $credPKey 294 * @param \stdClass $enc 295 * @throws WebAuthnException 296 */ 297 private function _readCredentialPublicKeyES256(&$credPKey, $enc) { 298 $credPKey->crv = $enc[self::$_COSE_CRV]; 299 $credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null; 300 $credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null; 301 unset ($enc); 302 303 // Validation 304 if ($credPKey->kty !== self::$_EC2_TYPE) { 305 throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY); 306 } 307 308 if ($credPKey->alg !== self::$_EC2_ES256) { 309 throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); 310 } 311 312 if ($credPKey->crv !== self::$_EC2_P256) { 313 throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY); 314 } 315 316 if (\strlen($credPKey->x) !== 32) { 317 throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); 318 } 319 320 if (\strlen($credPKey->y) !== 32) { 321 throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); 322 } 323 } 324 325 /** 326 * extract RS256 informations from COSE 327 * @param \stdClass $credPKey 328 * @param \stdClass $enc 329 * @throws WebAuthnException 330 */ 331 private function _readCredentialPublicKeyRS256(&$credPKey, $enc) { 332 $credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null; 333 $credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null; 334 unset ($enc); 335 336 // Validation 337 if ($credPKey->kty !== self::$_RSA_TYPE) { 338 throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY); 339 } 340 341 if ($credPKey->alg !== self::$_RSA_RS256) { 342 throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); 343 } 344 345 if (\strlen($credPKey->n) !== 256) { 346 throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY); 347 } 348 349 if (\strlen($credPKey->e) !== 3) { 350 throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY); 351 } 352 353 } 354 355 /** 356 * reads cbor encoded extension data. 357 * @param string $binary 358 * @return array 359 * @throws WebAuthnException 360 */ 361 private function _readExtensionData($binary) { 362 $ext = CborDecoder::decode($binary); 363 if (!\is_array($ext)) { 364 throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA); 365 } 366 367 return $ext; 368 } 369 370 371 // --------------- 372 // DER functions 373 // --------------- 374 375 private function _der_length($len) { 376 if ($len < 128) { 377 return \chr($len); 378 } 379 $lenBytes = ''; 380 while ($len > 0) { 381 $lenBytes = \chr($len % 256) . $lenBytes; 382 $len = \intdiv($len, 256); 383 } 384 return \chr(0x80 | \strlen($lenBytes)) . $lenBytes; 385 } 386 387 private function _der_sequence($contents) { 388 return "\x30" . $this->_der_length(\strlen($contents)) . $contents; 389 } 390 391 private function _der_oid($encoded) { 392 return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded; 393 } 394 395 private function _der_bitString($bytes) { 396 return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes; 397 } 398 399 private function _der_nullValue() { 400 return "\x05\x00"; 401 } 402 403 private function _der_unsignedInteger($bytes) { 404 $len = \strlen($bytes); 405 406 // Remove leading zero bytes 407 for ($i = 0; $i < ($len - 1); $i++) { 408 if (\ord($bytes[$i]) !== 0) { 409 break; 410 } 411 } 412 if ($i !== 0) { 413 $bytes = \substr($bytes, $i); 414 } 415 416 // If most significant bit is set, prefix with another zero to prevent it being seen as negative number 417 if ((\ord($bytes[0]) & 0x80) !== 0) { 418 $bytes = "\x00" . $bytes; 419 } 420 421 return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes; 422 } 423 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body