1 <?php 2 3 4 namespace lbuchs\WebAuthn\Attestation\Format; 5 use lbuchs\WebAuthn\Attestation\AuthenticatorData; 6 use lbuchs\WebAuthn\WebAuthnException; 7 use lbuchs\WebAuthn\Binary\ByteBuffer; 8 9 class AndroidSafetyNet extends FormatBase { 10 private $_signature; 11 private $_signedValue; 12 private $_x5c; 13 private $_payload; 14 15 public function __construct($AttestionObject, AuthenticatorData $authenticatorData) { 16 parent::__construct($AttestionObject, $authenticatorData); 17 18 // check data 19 $attStmt = $this->_attestationObject['attStmt']; 20 21 if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) { 22 throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); 23 } 24 25 if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) { 26 throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); 27 } 28 29 $response = $attStmt['response']->getBinaryString(); 30 31 // Response is a JWS [RFC7515] object in Compact Serialization. 32 // JWSs have three segments separated by two period ('.') characters 33 $parts = \explode('.', $response); 34 unset ($response); 35 if (\count($parts) !== 3) { 36 throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA); 37 } 38 39 $header = $this->_base64url_decode($parts[0]); 40 $payload = $this->_base64url_decode($parts[1]); 41 $this->_signature = $this->_base64url_decode($parts[2]); 42 $this->_signedValue = $parts[0] . '.' . $parts[1]; 43 unset ($parts); 44 45 $header = \json_decode($header); 46 $payload = \json_decode($payload); 47 48 if (!($header instanceof \stdClass)) { 49 throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA); 50 } 51 if (!($payload instanceof \stdClass)) { 52 throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA); 53 } 54 55 if (!isset($header->x5c) || !is_array($header->x5c) || count($header->x5c) === 0) { 56 throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA); 57 } 58 59 // algorithm 60 if (!\in_array($header->alg, array('RS256', 'ES256'))) { 61 throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA); 62 } 63 64 $this->_x5c = \base64_decode($header->x5c[0]); 65 $this->_payload = $payload; 66 67 if (count($header->x5c) > 1) { 68 for ($i=1; $i<count($header->x5c); $i++) { 69 $this->_x5c_chain[] = \base64_decode($header->x5c[$i]); 70 } 71 unset ($i); 72 } 73 } 74 75 /** 76 * ctsProfileMatch: A stricter verdict of device integrity. 77 * If the value of ctsProfileMatch is true, then the profile of the device running your app matches 78 * the profile of a device that has passed Android compatibility testing and 79 * has been approved as a Google-certified Android device. 80 * @return bool 81 */ 82 public function ctsProfileMatch() { 83 return isset($this->_payload->ctsProfileMatch) ? !!$this->_payload->ctsProfileMatch : false; 84 } 85 86 87 /* 88 * returns the key certificate in PEM format 89 * @return string 90 */ 91 public function getCertificatePem() { 92 return $this->_createCertificatePem($this->_x5c); 93 } 94 95 /** 96 * @param string $clientDataHash 97 */ 98 public function validateAttestation($clientDataHash) { 99 $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); 100 101 // Verify that the nonce in the response is identical to the Base64 encoding 102 // of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash. 103 if (empty($this->_payload->nonce) || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) { 104 throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA); 105 } 106 107 // Verify that attestationCert is issued to the hostname "attest.android.com" 108 $certInfo = \openssl_x509_parse($this->getCertificatePem()); 109 if (!\is_array($certInfo) || ($certInfo['subject']['CN'] ?? '') !== 'attest.android.com') { 110 throw new WebAuthnException('invalid certificate CN in JWS (' . ($certInfo['subject']['CN'] ?? '-'). ')', WebAuthnException::INVALID_DATA); 111 } 112 113 // Verify that the basicIntegrity attribute in the payload of response is true. 114 if (empty($this->_payload->basicIntegrity)) { 115 throw new WebAuthnException('invalid basicIntegrity in payload', WebAuthnException::INVALID_DATA); 116 } 117 118 // check certificate 119 return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; 120 } 121 122 123 /** 124 * validates the certificate against root certificates 125 * @param array $rootCas 126 * @return boolean 127 * @throws WebAuthnException 128 */ 129 public function validateRootCertificate($rootCas) { 130 $chainC = $this->_createX5cChainFile(); 131 if ($chainC) { 132 $rootCas[] = $chainC; 133 } 134 135 $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); 136 if ($v === -1) { 137 throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); 138 } 139 return $v; 140 } 141 142 143 /** 144 * decode base64 url 145 * @param string $data 146 * @return string 147 */ 148 private function _base64url_decode($data) { 149 return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4)); 150 } 151 } 152
title
Description
Body
title
Description
Body
title
Description
Body
title
Body