AuthenticatorData.php 13 KB

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