diff --git a/NEWS b/NEWS index 55e35f5..29d56aa 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ * Removed initial empty line from output for all commands. + * Use LF as EOL consistently. + * Updated release procedure. * Version 2.22 (released 2013-03-12) diff --git a/ykval-common.php b/ykval-common.php index adde24d..114a397 100644 --- a/ykval-common.php +++ b/ykval-common.php @@ -1,272 +1,272 @@ -log(LOG_INFO, $str); - die($str . "\n"); -} - -function unescape($s) { - return str_replace('\\', "", $s); -} - -function getHttpVal($key, $defaultVal) { - $val = $defaultVal; - if (array_key_exists($key, $_GET)) { - $val = $_GET[$key]; - } else if (array_key_exists($key, $_POST)) { - $val = $_POST[$key]; - } - $v = unescape(trim($val)); - return $v; -} - -function log_format() { - $str = ""; - foreach (func_get_args() as $msg) - { - if (is_array($msg)) { - foreach($msg as $key => $value){ - $str .= "$key=$value "; - } - } else { - $str .= $msg . " "; - } - } - return $str; -} - -// Return eg. 2008-11-21T06:11:55Z0711 -// -function getUTCTimeStamp() { - date_default_timezone_set('UTC'); - $tiny = substr(microtime(false), 2, 3); - return date('Y-m-d\TH:i:s\Z0', time()) . $tiny; -} - -# NOTE: When we evolve to using general DB-interface, this functinality -# should be moved there. -function DbTimeToUnix($db_time) -{ - $unix=strptime($db_time, '%F %H:%M:%S'); - return mktime($unix[tm_hour], $unix[tm_min], $unix[tm_sec], $unix[tm_mon]+1, $unix[tm_mday], $unix[tm_year]+1900); -} - -function UnixToDbTime($unix) -{ - return date('Y-m-d H:i:s', $unix); -} - -// Sign a http query string in the array of key-value pairs -// return b64 encoded hmac hash -function sign($a, $apiKey, $logger) { - ksort($a); - $qs = urldecode(http_build_query($a)); - - // the TRUE at the end states we want the raw value, not hexadecimal form - $hmac = hash_hmac('sha1', utf8_encode($qs), $apiKey, true); - $hmac = base64_encode($hmac); - - $logger->log(LOG_DEBUG, 'SIGN: ' . $qs . ' H=' . $hmac); - - return $hmac; - -} // sign an array of query string - -function hex2b64 ($hex_str) { - $bin = pack("H*", $hex_str); - return base64_encode($bin); -} - -function modhex2b64 ($modhex_str) { - $hex_str = strtr ($modhex_str, "cbdefghijklnrtuv", "0123456789abcdef"); - return hex2b64($hex_str); -} - -// This function takes a list of URLs. It will return the content of -// the first successfully retrieved URL, whose content matches ^OK. -// The request are sent asynchronously. Some of the URLs can fail -// with unknown host, connection errors, or network timeout, but as -// long as one of the URLs given work, data will be returned. If all -// URLs fail, data from some URL that did not match parameter $match -// (defaults to ^OK) is returned, or if all URLs failed, false. -function retrieveURLasync ($ident, $urls, $logger, $ans_req=1, $match="^OK", $returl=False, $timeout=10) { - $mh = curl_multi_init(); - - $ch = array(); - foreach ($urls as $id => $url) { - $handle = curl_init(); - $logger->log(LOG_DEBUG, $ident . " adding URL : " . $url); - curl_setopt($handle, CURLOPT_URL, $url); - curl_setopt($handle, CURLOPT_USERAGENT, "YK-VAL"); - curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($handle, CURLOPT_FAILONERROR, true); - curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); - - curl_multi_add_handle($mh, $handle); - - $ch[$handle] = $handle; - } - - $str = false; - $ans_count = 0; - $ans_arr = array(); - - do { - while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM) - ; - - while ($info = curl_multi_info_read($mh)) { - $logger->log(LOG_DEBUG, $ident . " curl multi info : ", $info); - if ($info['result'] == CURLE_OK) { - $str = curl_multi_getcontent($info['handle']); - $logger->log(LOG_DEBUG, $ident . " curl multi content : " . $str); - if (preg_match("/".$match."/", $str)) { - $logger->log(LOG_DEBUG, $ident . " response matches " . $match); - $error = curl_error ($info['handle']); - $errno = curl_errno ($info['handle']); - $cinfo = curl_getinfo ($info['handle']); - $logger->log(LOG_DEBUG, $ident . " errno/error: " . $errno . "/" . $error, $cinfo); - $ans_count++; - if ($returl) $ans_arr[]="url=" . $cinfo['url'] . "\n" . $str; - else $ans_arr[]=$str; - } - - if ($ans_count >= $ans_req) { - foreach ($ch as $h) { - curl_multi_remove_handle ($mh, $h); - curl_close ($h); - } - curl_multi_close ($mh); - - return $ans_arr; - } - - curl_multi_remove_handle ($mh, $info['handle']); - curl_close ($info['handle']); - unset ($ch[$info['handle']]); - } - - curl_multi_select ($mh); - } - } while($active); - - foreach ($ch as $h) { - curl_multi_remove_handle ($mh, $h); - curl_close ($h); - } - curl_multi_close ($mh); - - if ($ans_count>0) return $ans_arr; - return $str; -} - -function retrieveURLsimple ($url, $match="^OK") { - foreach (file($url) as $line) { - if (preg_match("/".$match."/", $line)) { - return $line; - } - } - return false; -} - -// $otp: A yubikey OTP -function KSMdecryptOTP($urls, $logger) { - $ret = array(); - if (!is_array($urls)) { - $response = retrieveURLsimple ($urls); - } elseif (count($urls) == 1) { - $response = retrieveURLsimple ($urls[0]); - } else { - $response = retrieveURLasync ("YK-KSM", $urls, $logger, $ans_req=1, $match="^OK", $returl=False, $timeout=10); - if (is_array($response)) { - $response = $response[0]; - } - } - if ($response) { - $logger->log(LOG_DEBUG, log_format("YK-KSM response: ", $response)); - } - if (sscanf ($response, - "OK counter=%04x low=%04x high=%02x use=%02x", - $ret["session_counter"], $ret["low"], $ret["high"], - $ret["session_use"]) != 4) { - return false; - } - return $ret; -} // End decryptOTP - -function sendResp($status, $logger, $apiKey = '', $extra = null) { - if ($status == null) { - $status = S_BACKEND_ERROR; - } - - $a['status'] = $status; - $a['t'] = getUTCTimeStamp(); - if ($extra){ - foreach ($extra as $param => $value) $a[$param] = $value; - } - $h = sign($a, $apiKey, $logger); - - $str = "h=" . $h . "\r\n"; - $str .= "t=" . ($a['t']) . "\r\n"; - if ($extra){ - foreach ($extra as $param => $value) { - $str .= $param . "=" . $value . "\r\n"; - } - } - $str .= "status=" . ($a['status']) . "\r\n"; - $str .= "\r\n"; - - $logger->log(LOG_INFO, "Response: " . $str . - " (at " . date("c") . " " . microtime() . ")"); - - echo $str; -} -?> +log(LOG_INFO, $str); + die($str . "\n"); +} + +function unescape($s) { + return str_replace('\\', "", $s); +} + +function getHttpVal($key, $defaultVal) { + $val = $defaultVal; + if (array_key_exists($key, $_GET)) { + $val = $_GET[$key]; + } else if (array_key_exists($key, $_POST)) { + $val = $_POST[$key]; + } + $v = unescape(trim($val)); + return $v; +} + +function log_format() { + $str = ""; + foreach (func_get_args() as $msg) + { + if (is_array($msg)) { + foreach($msg as $key => $value){ + $str .= "$key=$value "; + } + } else { + $str .= $msg . " "; + } + } + return $str; +} + +// Return eg. 2008-11-21T06:11:55Z0711 +// +function getUTCTimeStamp() { + date_default_timezone_set('UTC'); + $tiny = substr(microtime(false), 2, 3); + return date('Y-m-d\TH:i:s\Z0', time()) . $tiny; +} + +# NOTE: When we evolve to using general DB-interface, this functinality +# should be moved there. +function DbTimeToUnix($db_time) +{ + $unix=strptime($db_time, '%F %H:%M:%S'); + return mktime($unix[tm_hour], $unix[tm_min], $unix[tm_sec], $unix[tm_mon]+1, $unix[tm_mday], $unix[tm_year]+1900); +} + +function UnixToDbTime($unix) +{ + return date('Y-m-d H:i:s', $unix); +} + +// Sign a http query string in the array of key-value pairs +// return b64 encoded hmac hash +function sign($a, $apiKey, $logger) { + ksort($a); + $qs = urldecode(http_build_query($a)); + + // the TRUE at the end states we want the raw value, not hexadecimal form + $hmac = hash_hmac('sha1', utf8_encode($qs), $apiKey, true); + $hmac = base64_encode($hmac); + + $logger->log(LOG_DEBUG, 'SIGN: ' . $qs . ' H=' . $hmac); + + return $hmac; + +} // sign an array of query string + +function hex2b64 ($hex_str) { + $bin = pack("H*", $hex_str); + return base64_encode($bin); +} + +function modhex2b64 ($modhex_str) { + $hex_str = strtr ($modhex_str, "cbdefghijklnrtuv", "0123456789abcdef"); + return hex2b64($hex_str); +} + +// This function takes a list of URLs. It will return the content of +// the first successfully retrieved URL, whose content matches ^OK. +// The request are sent asynchronously. Some of the URLs can fail +// with unknown host, connection errors, or network timeout, but as +// long as one of the URLs given work, data will be returned. If all +// URLs fail, data from some URL that did not match parameter $match +// (defaults to ^OK) is returned, or if all URLs failed, false. +function retrieveURLasync ($ident, $urls, $logger, $ans_req=1, $match="^OK", $returl=False, $timeout=10) { + $mh = curl_multi_init(); + + $ch = array(); + foreach ($urls as $id => $url) { + $handle = curl_init(); + $logger->log(LOG_DEBUG, $ident . " adding URL : " . $url); + curl_setopt($handle, CURLOPT_URL, $url); + curl_setopt($handle, CURLOPT_USERAGENT, "YK-VAL"); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($handle, CURLOPT_FAILONERROR, true); + curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); + + curl_multi_add_handle($mh, $handle); + + $ch[$handle] = $handle; + } + + $str = false; + $ans_count = 0; + $ans_arr = array(); + + do { + while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM) + ; + + while ($info = curl_multi_info_read($mh)) { + $logger->log(LOG_DEBUG, $ident . " curl multi info : ", $info); + if ($info['result'] == CURLE_OK) { + $str = curl_multi_getcontent($info['handle']); + $logger->log(LOG_DEBUG, $ident . " curl multi content : " . $str); + if (preg_match("/".$match."/", $str)) { + $logger->log(LOG_DEBUG, $ident . " response matches " . $match); + $error = curl_error ($info['handle']); + $errno = curl_errno ($info['handle']); + $cinfo = curl_getinfo ($info['handle']); + $logger->log(LOG_DEBUG, $ident . " errno/error: " . $errno . "/" . $error, $cinfo); + $ans_count++; + if ($returl) $ans_arr[]="url=" . $cinfo['url'] . "\n" . $str; + else $ans_arr[]=$str; + } + + if ($ans_count >= $ans_req) { + foreach ($ch as $h) { + curl_multi_remove_handle ($mh, $h); + curl_close ($h); + } + curl_multi_close ($mh); + + return $ans_arr; + } + + curl_multi_remove_handle ($mh, $info['handle']); + curl_close ($info['handle']); + unset ($ch[$info['handle']]); + } + + curl_multi_select ($mh); + } + } while($active); + + foreach ($ch as $h) { + curl_multi_remove_handle ($mh, $h); + curl_close ($h); + } + curl_multi_close ($mh); + + if ($ans_count>0) return $ans_arr; + return $str; +} + +function retrieveURLsimple ($url, $match="^OK") { + foreach (file($url) as $line) { + if (preg_match("/".$match."/", $line)) { + return $line; + } + } + return false; +} + +// $otp: A yubikey OTP +function KSMdecryptOTP($urls, $logger) { + $ret = array(); + if (!is_array($urls)) { + $response = retrieveURLsimple ($urls); + } elseif (count($urls) == 1) { + $response = retrieveURLsimple ($urls[0]); + } else { + $response = retrieveURLasync ("YK-KSM", $urls, $logger, $ans_req=1, $match="^OK", $returl=False, $timeout=10); + if (is_array($response)) { + $response = $response[0]; + } + } + if ($response) { + $logger->log(LOG_DEBUG, log_format("YK-KSM response: ", $response)); + } + if (sscanf ($response, + "OK counter=%04x low=%04x high=%02x use=%02x", + $ret["session_counter"], $ret["low"], $ret["high"], + $ret["session_use"]) != 4) { + return false; + } + return $ret; +} // End decryptOTP + +function sendResp($status, $logger, $apiKey = '', $extra = null) { + if ($status == null) { + $status = S_BACKEND_ERROR; + } + + $a['status'] = $status; + $a['t'] = getUTCTimeStamp(); + if ($extra){ + foreach ($extra as $param => $value) $a[$param] = $value; + } + $h = sign($a, $apiKey, $logger); + + $str = "h=" . $h . "\r\n"; + $str .= "t=" . ($a['t']) . "\r\n"; + if ($extra){ + foreach ($extra as $param => $value) { + $str .= $param . "=" . $value . "\r\n"; + } + } + $str .= "status=" . ($a['status']) . "\r\n"; + $str .= "\r\n"; + + $logger->log(LOG_INFO, "Response: " . $str . + " (at " . date("c") . " " . microtime() . ")"); + + echo $str; +} +?> diff --git a/ykval-config.php b/ykval-config.php index 0952ab9..3cb331c 100644 --- a/ykval-config.php +++ b/ykval-config.php @@ -1,101 +1,101 @@ - PDO::ERRMODE_EXCEPTION); - -# For the validation server sync -$baseParams['__YKVAL_SYNC_POOL__'] = array(/*"http://api2.example.com/wsapi/2.0/sync",*/ - /*"http://api3.example.com/wsapi/2.0/sync",*/ - /* "http://api4.example.com/wsapi/2.0/sync"*/); -# An array of IP addresses allowed to issue sync requests -# NOTE: You must use IP addresses here. -$baseParams['__YKVAL_ALLOWED_SYNC_POOL__'] = array(/*"1.2.3.4",*/ - /*"2.3.4.5",*/ - /*"3.4.5.6"*/); - -# An array of IP addresses allowed to issue YubiKey activation/deactivation -# requests through ykval-revoke.php. NOTE: You must use IP addresses here. -$baseParams['__YKREV_IPS__'] = array(/*"127.0.0.1"*/); -# An array of IP addresses allowed to issue database resync requests through -# ykval-resync.php. NOTE: You must use IP addresses here. -#$baseParams['__YKRESYNC_IPS__'] = array("127.0.0.1"); -#Use the same as for issuing sync requests: -$baseParams['__YKRESYNC_IPS__'] = $baseParams['__YKVAL_ALLOWED_SYNC_POOL__']; - -# Specify how often the sync daemon awakens -$baseParams['__YKVAL_SYNC_INTERVAL__'] = 10; -# Specify how long the sync daemon will wait for response -$baseParams['__YKVAL_SYNC_RESYNC_TIMEOUT__'] = 30; -# Specify how old entries in the database should be considered aborted attempts -$baseParams['__YKVAL_SYNC_OLD_LIMIT__'] = 10; - -# These are settings for the validation server. -$baseParams['__YKVAL_SYNC_FAST_LEVEL__'] = 1; -$baseParams['__YKVAL_SYNC_SECURE_LEVEL__'] = 40; -$baseParams['__YKVAL_SYNC_DEFAULT_LEVEL__'] = 60; -$baseParams['__YKVAL_SYNC_DEFAULT_TIMEOUT__'] = 1; - -// otp2ksmurls: Return array of YK-KSM URLs for decrypting OTP for -// CLIENT. The URLs must be fully qualified, i.e., contain the OTP -// itself. -function otp2ksmurls ($otp, $client) { - //if ($client == 42) { - // return array("http://another-ykkms.example.com/wsapi/decrypt?otp=$otp"); - //} - - //if (preg_match ("/^dteffujehknh/", $otp)) { - // return array("http://different-ykkms.example.com/wsapi/decrypt?otp=$otp"); - //} - - return array( - //"http://ykkms1.example.com/wsapi/decrypt?otp=$otp", - //"http://ykkms2.example.com/wsapi/decrypt?otp=$otp", - "http://127.0.0.1/wsapi/decrypt?otp=$otp" - ); -} - -?> + PDO::ERRMODE_EXCEPTION); + +# For the validation server sync +$baseParams['__YKVAL_SYNC_POOL__'] = array(/*"http://api2.example.com/wsapi/2.0/sync",*/ + /*"http://api3.example.com/wsapi/2.0/sync",*/ + /* "http://api4.example.com/wsapi/2.0/sync"*/); +# An array of IP addresses allowed to issue sync requests +# NOTE: You must use IP addresses here. +$baseParams['__YKVAL_ALLOWED_SYNC_POOL__'] = array(/*"1.2.3.4",*/ + /*"2.3.4.5",*/ + /*"3.4.5.6"*/); + +# An array of IP addresses allowed to issue YubiKey activation/deactivation +# requests through ykval-revoke.php. NOTE: You must use IP addresses here. +$baseParams['__YKREV_IPS__'] = array(/*"127.0.0.1"*/); +# An array of IP addresses allowed to issue database resync requests through +# ykval-resync.php. NOTE: You must use IP addresses here. +#$baseParams['__YKRESYNC_IPS__'] = array("127.0.0.1"); +#Use the same as for issuing sync requests: +$baseParams['__YKRESYNC_IPS__'] = $baseParams['__YKVAL_ALLOWED_SYNC_POOL__']; + +# Specify how often the sync daemon awakens +$baseParams['__YKVAL_SYNC_INTERVAL__'] = 10; +# Specify how long the sync daemon will wait for response +$baseParams['__YKVAL_SYNC_RESYNC_TIMEOUT__'] = 30; +# Specify how old entries in the database should be considered aborted attempts +$baseParams['__YKVAL_SYNC_OLD_LIMIT__'] = 10; + +# These are settings for the validation server. +$baseParams['__YKVAL_SYNC_FAST_LEVEL__'] = 1; +$baseParams['__YKVAL_SYNC_SECURE_LEVEL__'] = 40; +$baseParams['__YKVAL_SYNC_DEFAULT_LEVEL__'] = 60; +$baseParams['__YKVAL_SYNC_DEFAULT_TIMEOUT__'] = 1; + +// otp2ksmurls: Return array of YK-KSM URLs for decrypting OTP for +// CLIENT. The URLs must be fully qualified, i.e., contain the OTP +// itself. +function otp2ksmurls ($otp, $client) { + //if ($client == 42) { + // return array("http://another-ykkms.example.com/wsapi/decrypt?otp=$otp"); + //} + + //if (preg_match ("/^dteffujehknh/", $otp)) { + // return array("http://different-ykkms.example.com/wsapi/decrypt?otp=$otp"); + //} + + return array( + //"http://ykkms1.example.com/wsapi/decrypt?otp=$otp", + //"http://ykkms2.example.com/wsapi/decrypt?otp=$otp", + "http://127.0.0.1/wsapi/decrypt?otp=$otp" + ); +} + +?> diff --git a/ykval-sync.php b/ykval-sync.php index 27cc0c5..ead2e11 100644 --- a/ykval-sync.php +++ b/ykval-sync.php @@ -1,210 +1,210 @@ -addField('ip', $_SERVER['REMOTE_ADDR']); - -if(empty($_SERVER['QUERY_STRING'])) { - sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); - exit; -} - -$myLog->log(LOG_INFO, "Request: " . $_SERVER['QUERY_STRING']); - -$sync = new SyncLib('ykval-sync:synclib'); -$sync->addField('ip', $_SERVER['REMOTE_ADDR']); - -if (! $sync->isConnected()) { - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; -} - -# -# Verify that request comes from valid server -# - -$myLog->log(LOG_INFO, 'Received request from ' . $_SERVER['REMOTE_ADDR']); - -$allowed = in_array($_SERVER['REMOTE_ADDR'], $baseParams['__YKVAL_ALLOWED_SYNC_POOL__']); - -if (!$allowed) { - $myLog->log(LOG_NOTICE, 'Operation not allowed from IP ' . $_SERVER['REMOTE_ADDR']); - $myLog->log(LOG_DEBUG, 'Remote IP ' . $_SERVER['REMOTE_ADDR'] . ' not listed in allowed sync pool : ' . - implode(', ', $baseParams['__YKVAL_ALLOWED_SYNC_POOL__'])); - sendResp(S_OPERATION_NOT_ALLOWED, $myLog, $apiKey); - exit; -} - -# -# Define requirements on protocol -# - -$syncParams=array('modified'=>Null, - 'otp'=>Null, - 'nonce'=>Null, - 'yk_publicname'=>Null, - 'yk_counter'=>Null, - 'yk_use'=>Null, - 'yk_high'=>Null, - 'yk_low'=>Null); - -# -# Extract values from HTTP request -# - -$tmp_log = "Received "; -foreach ($syncParams as $param=>$value) { - $value = getHttpVal($param, Null); - if ($value==Null) { - $myLog->log(LOG_NOTICE, "Received request with parameter[s] (" . $param . ") missing value"); - sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); - exit; - } - $syncParams[$param]=$value; - $tmp_log .= "$param=$value "; -} -$myLog->log(LOG_INFO, $tmp_log); - -# -# At this point we should have the otp so let's add it to the logging module -# -$myLog->addField('otp', $syncParams['otp']); -$sync->addField('otp', $syncParams['otp']); - -# -# Verify correctness of input parameters -# - -foreach (array('modified') as $param) { - if (preg_match("/^[0-9]+$/", $syncParams[$param])==0) { - $myLog->log(LOG_NOTICE, 'Input parameters ' . $param . ' not correct'); - sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); - exit; - } -} - -foreach (array('yk_counter', 'yk_use', 'yk_high', 'yk_low') as $param) { - if (preg_match("/^(-1|[0-9]+)$/", $syncParams[$param])==0) { - $myLog->log(LOG_NOTICE, 'Input parameters ' . $param . ' not correct'); - sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); - exit; - } -} - - - - -# -# Get local counter data -# - -$yk_publicname = $syncParams['yk_publicname']; -$localParams = $sync->getLocalParams($yk_publicname); -if (!$localParams) { - $myLog->log(LOG_NOTICE, 'Invalid Yubikey ' . $yk_publicname); - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; -} - -/* Conditional update local database */ -$sync->updateDbCounters($syncParams); - -$myLog->log(LOG_DEBUG, 'Local params ' , $localParams); -$myLog->log(LOG_DEBUG, 'Sync request params ' , $syncParams); - -# -# Compare sync and local counters and generate warnings according to -# -# https://github.com/Yubico/yubikey-val/wiki/ServerReplicationProtocol -# - - - -if ($sync->countersHigherThan($localParams, $syncParams)) { - $myLog->log(LOG_WARNING, 'Remote server out of sync.'); -} - - -if ($sync->countersEqual($localParams, $syncParams)) { - - if ($syncParams['modified']==$localParams['modified'] && - $syncParams['nonce']==$localParams['nonce']) { - /* This is not an error. When the remote server received an OTP to verify, it would - * have sent out sync requests immediately. When the required number of responses had - * been received, the current implementation discards all additional responses (to - * return the result to the client as soon as possible). If our response sent last - * time was discarded, we will end up here when the background ykval-queue processes - * the sync request again. - */ - $myLog->log(LOG_INFO, 'Sync request unnecessarily sent'); - } - - if ($syncParams['modified']!=$localParams['modified'] && - $syncParams['nonce']==$localParams['nonce']) { - $deltaModified = $syncParams['modified'] - $localParams['modified']; - if($deltaModified < -1 || $deltaModified > 1) { - $myLog->log(LOG_WARNING, 'We might have a replay. 2 events at different times have generated the same counters. The time difference is ' . $deltaModified . ' seconds'); - } - } - - if ($syncParams['nonce']!=$localParams['nonce']) { - $myLog->log(LOG_WARNING, 'Remote server has received a request to validate an already validated OTP '); - } -} - -if ($localParams['active'] != 1) { - /* The remote server has accepted an OTP from a YubiKey which we would not. - * We still needed to update our counters with the counters from the OTP though. - */ - $myLog->log(LOG_WARNING, 'Received sync-request for de-activated Yubikey ' . $yk_publicname . - ' - check database synchronization!!!'); - sendResp(S_BAD_OTP, $myLog, $apiKey); - exit; -} - -$extra=array('modified'=>$localParams['modified'], - 'nonce'=>$localParams['nonce'], - 'yk_publicname'=>$yk_publicname, - 'yk_counter'=>$localParams['yk_counter'], - 'yk_use'=>$localParams['yk_use'], - 'yk_high'=>$localParams['yk_high'], - 'yk_low'=>$localParams['yk_low']); - -sendResp(S_OK, $myLog, $apiKey, $extra); - -?> +addField('ip', $_SERVER['REMOTE_ADDR']); + +if(empty($_SERVER['QUERY_STRING'])) { + sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); + exit; +} + +$myLog->log(LOG_INFO, "Request: " . $_SERVER['QUERY_STRING']); + +$sync = new SyncLib('ykval-sync:synclib'); +$sync->addField('ip', $_SERVER['REMOTE_ADDR']); + +if (! $sync->isConnected()) { + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; +} + +# +# Verify that request comes from valid server +# + +$myLog->log(LOG_INFO, 'Received request from ' . $_SERVER['REMOTE_ADDR']); + +$allowed = in_array($_SERVER['REMOTE_ADDR'], $baseParams['__YKVAL_ALLOWED_SYNC_POOL__']); + +if (!$allowed) { + $myLog->log(LOG_NOTICE, 'Operation not allowed from IP ' . $_SERVER['REMOTE_ADDR']); + $myLog->log(LOG_DEBUG, 'Remote IP ' . $_SERVER['REMOTE_ADDR'] . ' not listed in allowed sync pool : ' . + implode(', ', $baseParams['__YKVAL_ALLOWED_SYNC_POOL__'])); + sendResp(S_OPERATION_NOT_ALLOWED, $myLog, $apiKey); + exit; +} + +# +# Define requirements on protocol +# + +$syncParams=array('modified'=>Null, + 'otp'=>Null, + 'nonce'=>Null, + 'yk_publicname'=>Null, + 'yk_counter'=>Null, + 'yk_use'=>Null, + 'yk_high'=>Null, + 'yk_low'=>Null); + +# +# Extract values from HTTP request +# + +$tmp_log = "Received "; +foreach ($syncParams as $param=>$value) { + $value = getHttpVal($param, Null); + if ($value==Null) { + $myLog->log(LOG_NOTICE, "Received request with parameter[s] (" . $param . ") missing value"); + sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); + exit; + } + $syncParams[$param]=$value; + $tmp_log .= "$param=$value "; +} +$myLog->log(LOG_INFO, $tmp_log); + +# +# At this point we should have the otp so let's add it to the logging module +# +$myLog->addField('otp', $syncParams['otp']); +$sync->addField('otp', $syncParams['otp']); + +# +# Verify correctness of input parameters +# + +foreach (array('modified') as $param) { + if (preg_match("/^[0-9]+$/", $syncParams[$param])==0) { + $myLog->log(LOG_NOTICE, 'Input parameters ' . $param . ' not correct'); + sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); + exit; + } +} + +foreach (array('yk_counter', 'yk_use', 'yk_high', 'yk_low') as $param) { + if (preg_match("/^(-1|[0-9]+)$/", $syncParams[$param])==0) { + $myLog->log(LOG_NOTICE, 'Input parameters ' . $param . ' not correct'); + sendResp(S_MISSING_PARAMETER, $myLog, $apiKey); + exit; + } +} + + + + +# +# Get local counter data +# + +$yk_publicname = $syncParams['yk_publicname']; +$localParams = $sync->getLocalParams($yk_publicname); +if (!$localParams) { + $myLog->log(LOG_NOTICE, 'Invalid Yubikey ' . $yk_publicname); + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; +} + +/* Conditional update local database */ +$sync->updateDbCounters($syncParams); + +$myLog->log(LOG_DEBUG, 'Local params ' , $localParams); +$myLog->log(LOG_DEBUG, 'Sync request params ' , $syncParams); + +# +# Compare sync and local counters and generate warnings according to +# +# https://github.com/Yubico/yubikey-val/wiki/ServerReplicationProtocol +# + + + +if ($sync->countersHigherThan($localParams, $syncParams)) { + $myLog->log(LOG_WARNING, 'Remote server out of sync.'); +} + + +if ($sync->countersEqual($localParams, $syncParams)) { + + if ($syncParams['modified']==$localParams['modified'] && + $syncParams['nonce']==$localParams['nonce']) { + /* This is not an error. When the remote server received an OTP to verify, it would + * have sent out sync requests immediately. When the required number of responses had + * been received, the current implementation discards all additional responses (to + * return the result to the client as soon as possible). If our response sent last + * time was discarded, we will end up here when the background ykval-queue processes + * the sync request again. + */ + $myLog->log(LOG_INFO, 'Sync request unnecessarily sent'); + } + + if ($syncParams['modified']!=$localParams['modified'] && + $syncParams['nonce']==$localParams['nonce']) { + $deltaModified = $syncParams['modified'] - $localParams['modified']; + if($deltaModified < -1 || $deltaModified > 1) { + $myLog->log(LOG_WARNING, 'We might have a replay. 2 events at different times have generated the same counters. The time difference is ' . $deltaModified . ' seconds'); + } + } + + if ($syncParams['nonce']!=$localParams['nonce']) { + $myLog->log(LOG_WARNING, 'Remote server has received a request to validate an already validated OTP '); + } +} + +if ($localParams['active'] != 1) { + /* The remote server has accepted an OTP from a YubiKey which we would not. + * We still needed to update our counters with the counters from the OTP though. + */ + $myLog->log(LOG_WARNING, 'Received sync-request for de-activated Yubikey ' . $yk_publicname . + ' - check database synchronization!!!'); + sendResp(S_BAD_OTP, $myLog, $apiKey); + exit; +} + +$extra=array('modified'=>$localParams['modified'], + 'nonce'=>$localParams['nonce'], + 'yk_publicname'=>$yk_publicname, + 'yk_counter'=>$localParams['yk_counter'], + 'yk_use'=>$localParams['yk_use'], + 'yk_high'=>$localParams['yk_high'], + 'yk_low'=>$localParams['yk_low']); + +sendResp(S_OK, $myLog, $apiKey, $extra); + +?> diff --git a/ykval-verify.php b/ykval-verify.php index d3b767c..588da70 100644 --- a/ykval-verify.php +++ b/ykval-verify.php @@ -1,422 +1,422 @@ -addField('ip', $_SERVER['REMOTE_ADDR']); -$query_string = ''; -if ($_POST) { - $kv = array(); - foreach ($_POST as $key => $value) { - $kv[] = "$key=$value"; - } - $query_string = "POST: " . join("&", $kv); -} else { - $query_string = "Request: " . $_SERVER['QUERY_STRING']; -} - -$myLog->log(LOG_INFO, $query_string . - " (at " . date("c") . " " . microtime() . ") " . - (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on" ? "HTTPS" : "HTTP")); - -/* Detect protocol version */ -if (preg_match("/\/wsapi\/([0-9]+)\.([0-9]+)\//", $_SERVER['REQUEST_URI'], $out)) { - $protocol_version=$out[1]+$out[2]*0.1; - } else { - $protocol_version=1.0; - } - -$myLog->log(LOG_DEBUG, "found protocol version " . $protocol_version); - -/* Extract values from HTTP request - */ -$h = getHttpVal('h', ''); -$client = getHttpVal('id', 0); -$otp = getHttpVal('otp', ''); -$otp = strtolower($otp); -if (preg_match("/^[jxe.uidchtnbpygk]+$/", $otp)) { - $new_otp = strtr($otp, "jxe.uidchtnbpygk", "cbdefghijklnrtuv"); - $myLog->log(LOG_INFO, 'Dvorak OTP converting ' . $otp . ' to ' . $new_otp); - $otp = $new_otp; -} -$timestamp = getHttpVal('timestamp', 0); - -/* Construct response parameters */ -$extra=array(); -if ($protocol_version>=2.0) { - $extra['otp']=$otp; -} - - -/* We have the OTP now, so let's add it to the logging */ -$myLog->addField('otp', $otp); - -if ($protocol_version>=2.0) { - $sl = getHttpVal('sl', ''); - $timeout = getHttpVal('timeout', ''); - $nonce = getHttpVal('nonce', ''); - - /* Add nonce to response parameters */ - $extra['nonce']= $nonce; - - /* Nonce is required from protocol 2.0 */ - if(!$nonce) { - $myLog->log(LOG_NOTICE, 'Nonce is missing and protocol version >= 2.0'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; - } -} - - -/* Sanity check HTTP parameters - - * otp: one-time password - * id: client id - * timeout: timeout in seconds to wait for external answers, optional: if absent the server decides - * nonce: random alphanumeric string, 16 to 40 characters long. Must be non-predictable and changing for each request, but need not be cryptographically strong - * sl: "sync level", percentage of external servers that needs to answer (integer 0 to 100), or "fast" or "secure" to use server-configured values - * h: signature (optional) - * timestamp: requests timestamp/counters in response - - */ - -/* Change default protocol "strings" to numeric values */ -if (isset($sl) && strcasecmp($sl, 'fast')==0) { - $sl=$baseParams['__YKVAL_SYNC_FAST_LEVEL__']; -} -if (isset($sl) && strcasecmp($sl, 'secure')==0) { - $sl=$baseParams['__YKVAL_SYNC_SECURE_LEVEL__']; -} -if (!isset($sl) || $sl == '') { - $sl=$baseParams['__YKVAL_SYNC_DEFAULT_LEVEL__']; -} -if (!isset($timeout) || $timeout == '') { - $timeout=$baseParams['__YKVAL_SYNC_DEFAULT_TIMEOUT__']; -} - -if ($otp == '') { - $myLog->log(LOG_NOTICE, 'OTP is missing'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -if (strlen($otp) < TOKEN_LEN || strlen ($otp) > OTP_MAX_LEN) { - $myLog->log(LOG_NOTICE, 'Incorrect OTP length: ' . $otp); - sendResp(S_BAD_OTP, $myLog); - exit; -} - -if (preg_match("/^[cbdefghijklnrtuv]+$/", $otp)==0) { - $myLog->log(LOG_NOTICE, 'Invalid OTP: ' . $otp); - sendResp(S_BAD_OTP, $myLog); - exit; -} - -if (preg_match("/^[0-9]+$/", $client)==0){ - $myLog->log(LOG_NOTICE, 'id provided in request must be an integer'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -if ($timeout && preg_match("/^[0-9]+$/", $timeout)==0) { - $myLog->log(LOG_NOTICE, 'timeout is provided but not correct'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -if (isset($nonce) && preg_match("/^[A-Za-z0-9]+$/", $nonce)==0) { - $myLog->log(LOG_NOTICE, 'NONCE is provided but not correct'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -if (isset($nonce) && (strlen($nonce) < 16 || strlen($nonce) > 40)) { - $myLog->log(LOG_NOTICE, 'Nonce too short or too long'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -if ($sl && (preg_match("/^[0-9]+$/", $sl)==0 || ($sl<0 || $sl>100))) { - $myLog->log(LOG_NOTICE, 'SL is provided but not correct'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - -// NOTE: Timestamp parameter is not checked since current protocol says that 1 means request timestamp -// and anything else is discarded. - -//// Get Client info from DB -// -if ($client <= 0) { - $myLog->log(LOG_NOTICE, 'Client ID is missing'); - sendResp(S_MISSING_PARAMETER, $myLog); - exit; -} - - - -/* Initialize the sync library. Strive to use this instead of custom - DB requests, custom comparisons etc */ -$sync = new SyncLib('ykval-verify:synclib'); -$sync->addField('ip', $_SERVER['REMOTE_ADDR']); -$sync->addField('otp', $otp); - -if (! $sync->isConnected()) { - sendResp(S_BACKEND_ERROR, $myLog); - exit; - } - -$cd=$sync->getClientData($client); -if(!$cd) { - $myLog->log(LOG_NOTICE, 'Invalid client id ' . $client); - sendResp(S_NO_SUCH_CLIENT, $myLog); - exit; - } -$myLog->log(LOG_DEBUG,"Client data:", $cd); - -//// Check client signature -// -$apiKey = base64_decode($cd['secret']); - -if ($h != '') { - // Create the signature using the API key - $a; - if($_GET) { - $a = $_GET; - } elseif($_POST) { - $a = $_POST; - } else { - sendRest(S_BACKEND_ERROR); - exit; - } - unset($a['h']); - - $hmac = sign($a, $apiKey, $myLog); - // Compare it - if ($hmac != $h) { - $myLog->log(LOG_DEBUG, 'client hmac=' . $h . ', server hmac=' . $hmac); - sendResp(S_BAD_SIGNATURE, $myLog, $apiKey); - exit; - } -} - -/* We need to add necessary parameters not available at earlier protocols after signature is computed. - */ -if ($protocol_version<2.0) { - /* We need to create a nonce manually here */ - $nonce = md5(uniqid(rand())); - $myLog->log(LOG_INFO, 'protocol version below 2.0. Created nonce ' . $nonce); - } - -//// Which YK-KSM should we talk to? -// -$urls = otp2ksmurls ($otp, $client); -if (!is_array($urls)) { - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; -} - -//// Decode OTP from input -// -$otpinfo = KSMdecryptOTP($urls, $myLog); -if (!is_array($otpinfo)) { - sendResp(S_BAD_OTP, $myLog, $apiKey); - exit; -} -$myLog->log(LOG_DEBUG, "Decrypted OTP:", $otpinfo); - -//// Get Yubikey from DB -// -$devId = substr($otp, 0, strlen ($otp) - TOKEN_LEN); -$yk_publicname=$devId; -$localParams = $sync->getLocalParams($yk_publicname); -if (!$localParams) { - $myLog->log(LOG_NOTICE, 'Invalid Yubikey ' . $yk_publicname); - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; - } - -$myLog->log(LOG_DEBUG, "Auth data:", $localParams); -if ($localParams['active'] != 1) { - $myLog->log(LOG_NOTICE, 'De-activated Yubikey ' . $devId); - sendResp(S_BAD_OTP, $myLog, $apiKey); - exit; -} - -/* Build OTP params */ - -$otpParams=array('modified'=>time(), - 'otp'=>$otp, - 'nonce'=>$nonce, - 'yk_publicname'=>$devId, - 'yk_counter'=>$otpinfo['session_counter'], - 'yk_use'=>$otpinfo['session_use'], - 'yk_high'=>$otpinfo['high'], - 'yk_low'=>$otpinfo['low']); - - -/* First check if OTP is seen with the same nonce, in such case we have an replayed request */ -if ($sync->countersEqual($localParams, $otpParams) && - $localParams['nonce']==$otpParams['nonce']) { - $myLog->log(LOG_WARNING, 'Replayed request'); - sendResp(S_REPLAYED_REQUEST, $myLog, $apiKey, $extra); - exit; - } - -/* Check the OTP counters against local db */ -if ($sync->countersHigherThanOrEqual($localParams, $otpParams)) { - $sync->log(LOG_WARNING, 'replayed OTP: Local counters higher'); - $sync->log(LOG_WARNING, 'replayed OTP: Local counters ', $localParams); - $sync->log(LOG_WARNING, 'replayed OTP: Otp counters ', $otpParams); - sendResp(S_REPLAYED_OTP, $myLog, $apiKey, $extra); - exit; - } - -/* Valid OTP, update database. */ - -if(!$sync->updateDbCounters($otpParams)) { - $myLog->log(LOG_CRIT, "Failed to update yubikey counters in database"); - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; - } - -/* Queue sync requests */ - -if (!$sync->queue($otpParams, $localParams)) { - $myLog->log(LOG_CRIT, "ykval-verify:critical:failed to queue sync requests"); - sendResp(S_BACKEND_ERROR, $myLog, $apiKey); - exit; - } - -$nr_servers=$sync->getNumberOfServers(); -$req_answers=ceil($nr_servers*$sl/100.0); -if ($req_answers>0) { - $syncres=$sync->sync($req_answers, $timeout); - $nr_answers=$sync->getNumberOfAnswers(); - $nr_valid_answers=$sync->getNumberOfValidAnswers(); - $sl_success_rate=floor(100.0 * $nr_valid_answers / $nr_servers); - - } else { - $syncres=true; - $nr_answers=0; - $nr_valid_answers=0; - $sl_success_rate=0; - } -$myLog->log(LOG_INFO, "ykval-verify:notice:synclevel=" . $sl . - " nr servers=" . $nr_servers . - " req answers=" . $req_answers . - " answers=" . $nr_answers . - " valid answers=" . $nr_valid_answers . - " sl success rate=" . $sl_success_rate . - " timeout=" . $timeout); - -if($syncres==False) { - /* sync returned false, indicating that - either at least 1 answer marked OTP as invalid or - there were not enough answers */ - $myLog->log(LOG_WARNING, "ykval-verify:notice:Sync failed"); - if ($nr_valid_answers!=$nr_answers) { - sendResp(S_REPLAYED_OTP, $myLog, $apiKey, $extra); - exit; - } else { - $extra['sl']=$sl_success_rate; - sendResp(S_NOT_ENOUGH_ANSWERS, $myLog, $apiKey, $extra); - exit; - } - } - -/* Recreate parameters to make phising test work out - TODO: use timefunctionality in deltatime library instead */ -$sessionCounter = $otpParams['yk_counter']; -$sessionUse = $otpParams['yk_use']; -$seenSessionCounter = $localParams['yk_counter']; -$seenSessionUse = $localParams['yk_use']; - -$ad['high']=$localParams['yk_high']; -$ad['low']=$localParams['yk_low']; -$ad['accessed']=$sync->unixToDbTime($localParams['modified']); - -//// Check the time stamp -// -if ($sessionCounter == $seenSessionCounter && $sessionUse > $seenSessionUse) { - $ts = ($otpinfo['high'] << 16) + $otpinfo['low']; - $seenTs = ($ad['high'] << 16) + $ad['low']; - $tsDiff = $ts - $seenTs; - $tsDelta = $tsDiff * TS_SEC; - - //// Check the real time - // - $lastTime = strtotime($ad['accessed']); - $now = time(); - $elapsed = $now - $lastTime; - $deviation = abs($elapsed - $tsDelta); - - // Time delta server might verify multiple OTPS in a row. In such case validation server doesn't - // have time to tick a whole second and we need to avoid division by zero. - if ($elapsed != 0) { - $percent = $deviation/$elapsed; - } else { - $percent = 1; - } - $myLog->log(LOG_INFO, "Timestamp seen=" . $seenTs . " this=" . $ts . - " delta=" . $tsDiff . ' secs=' . $tsDelta . - ' accessed=' . $lastTime .' (' . $ad['accessed'] . ') now=' - . $now . ' (' . strftime("%Y-%m-%d %H:%M:%S", $now) - . ') elapsed=' . $elapsed . - ' deviation=' . $deviation . ' secs or '. - round(100*$percent) . '%'); - if ($deviation > TS_ABS_TOLERANCE && $percent > TS_REL_TOLERANCE) { - $myLog->log(LOG_NOTICE, "OTP failed phishing test"); - if (0) { - sendResp(S_DELAYED_OTP, $myLog, $apiKey, $extra); - exit; - } - } -} - -/* Fill up with more respone parameters */ -if ($protocol_version>=2.0) { - $extra['sl'] = $sl_success_rate; - } -if ($timestamp==1){ - $extra['timestamp'] = ($otpinfo['high'] << 16) + $otpinfo['low']; - $extra['sessioncounter'] = $sessionCounter; - $extra['sessionuse'] = $sessionUse; - } - -sendResp(S_OK, $myLog, $apiKey, $extra); - -?> +addField('ip', $_SERVER['REMOTE_ADDR']); +$query_string = ''; +if ($_POST) { + $kv = array(); + foreach ($_POST as $key => $value) { + $kv[] = "$key=$value"; + } + $query_string = "POST: " . join("&", $kv); +} else { + $query_string = "Request: " . $_SERVER['QUERY_STRING']; +} + +$myLog->log(LOG_INFO, $query_string . + " (at " . date("c") . " " . microtime() . ") " . + (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on" ? "HTTPS" : "HTTP")); + +/* Detect protocol version */ +if (preg_match("/\/wsapi\/([0-9]+)\.([0-9]+)\//", $_SERVER['REQUEST_URI'], $out)) { + $protocol_version=$out[1]+$out[2]*0.1; + } else { + $protocol_version=1.0; + } + +$myLog->log(LOG_DEBUG, "found protocol version " . $protocol_version); + +/* Extract values from HTTP request + */ +$h = getHttpVal('h', ''); +$client = getHttpVal('id', 0); +$otp = getHttpVal('otp', ''); +$otp = strtolower($otp); +if (preg_match("/^[jxe.uidchtnbpygk]+$/", $otp)) { + $new_otp = strtr($otp, "jxe.uidchtnbpygk", "cbdefghijklnrtuv"); + $myLog->log(LOG_INFO, 'Dvorak OTP converting ' . $otp . ' to ' . $new_otp); + $otp = $new_otp; +} +$timestamp = getHttpVal('timestamp', 0); + +/* Construct response parameters */ +$extra=array(); +if ($protocol_version>=2.0) { + $extra['otp']=$otp; +} + + +/* We have the OTP now, so let's add it to the logging */ +$myLog->addField('otp', $otp); + +if ($protocol_version>=2.0) { + $sl = getHttpVal('sl', ''); + $timeout = getHttpVal('timeout', ''); + $nonce = getHttpVal('nonce', ''); + + /* Add nonce to response parameters */ + $extra['nonce']= $nonce; + + /* Nonce is required from protocol 2.0 */ + if(!$nonce) { + $myLog->log(LOG_NOTICE, 'Nonce is missing and protocol version >= 2.0'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; + } +} + + +/* Sanity check HTTP parameters + + * otp: one-time password + * id: client id + * timeout: timeout in seconds to wait for external answers, optional: if absent the server decides + * nonce: random alphanumeric string, 16 to 40 characters long. Must be non-predictable and changing for each request, but need not be cryptographically strong + * sl: "sync level", percentage of external servers that needs to answer (integer 0 to 100), or "fast" or "secure" to use server-configured values + * h: signature (optional) + * timestamp: requests timestamp/counters in response + + */ + +/* Change default protocol "strings" to numeric values */ +if (isset($sl) && strcasecmp($sl, 'fast')==0) { + $sl=$baseParams['__YKVAL_SYNC_FAST_LEVEL__']; +} +if (isset($sl) && strcasecmp($sl, 'secure')==0) { + $sl=$baseParams['__YKVAL_SYNC_SECURE_LEVEL__']; +} +if (!isset($sl) || $sl == '') { + $sl=$baseParams['__YKVAL_SYNC_DEFAULT_LEVEL__']; +} +if (!isset($timeout) || $timeout == '') { + $timeout=$baseParams['__YKVAL_SYNC_DEFAULT_TIMEOUT__']; +} + +if ($otp == '') { + $myLog->log(LOG_NOTICE, 'OTP is missing'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +if (strlen($otp) < TOKEN_LEN || strlen ($otp) > OTP_MAX_LEN) { + $myLog->log(LOG_NOTICE, 'Incorrect OTP length: ' . $otp); + sendResp(S_BAD_OTP, $myLog); + exit; +} + +if (preg_match("/^[cbdefghijklnrtuv]+$/", $otp)==0) { + $myLog->log(LOG_NOTICE, 'Invalid OTP: ' . $otp); + sendResp(S_BAD_OTP, $myLog); + exit; +} + +if (preg_match("/^[0-9]+$/", $client)==0){ + $myLog->log(LOG_NOTICE, 'id provided in request must be an integer'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +if ($timeout && preg_match("/^[0-9]+$/", $timeout)==0) { + $myLog->log(LOG_NOTICE, 'timeout is provided but not correct'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +if (isset($nonce) && preg_match("/^[A-Za-z0-9]+$/", $nonce)==0) { + $myLog->log(LOG_NOTICE, 'NONCE is provided but not correct'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +if (isset($nonce) && (strlen($nonce) < 16 || strlen($nonce) > 40)) { + $myLog->log(LOG_NOTICE, 'Nonce too short or too long'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +if ($sl && (preg_match("/^[0-9]+$/", $sl)==0 || ($sl<0 || $sl>100))) { + $myLog->log(LOG_NOTICE, 'SL is provided but not correct'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + +// NOTE: Timestamp parameter is not checked since current protocol says that 1 means request timestamp +// and anything else is discarded. + +//// Get Client info from DB +// +if ($client <= 0) { + $myLog->log(LOG_NOTICE, 'Client ID is missing'); + sendResp(S_MISSING_PARAMETER, $myLog); + exit; +} + + + +/* Initialize the sync library. Strive to use this instead of custom + DB requests, custom comparisons etc */ +$sync = new SyncLib('ykval-verify:synclib'); +$sync->addField('ip', $_SERVER['REMOTE_ADDR']); +$sync->addField('otp', $otp); + +if (! $sync->isConnected()) { + sendResp(S_BACKEND_ERROR, $myLog); + exit; + } + +$cd=$sync->getClientData($client); +if(!$cd) { + $myLog->log(LOG_NOTICE, 'Invalid client id ' . $client); + sendResp(S_NO_SUCH_CLIENT, $myLog); + exit; + } +$myLog->log(LOG_DEBUG,"Client data:", $cd); + +//// Check client signature +// +$apiKey = base64_decode($cd['secret']); + +if ($h != '') { + // Create the signature using the API key + $a; + if($_GET) { + $a = $_GET; + } elseif($_POST) { + $a = $_POST; + } else { + sendRest(S_BACKEND_ERROR); + exit; + } + unset($a['h']); + + $hmac = sign($a, $apiKey, $myLog); + // Compare it + if ($hmac != $h) { + $myLog->log(LOG_DEBUG, 'client hmac=' . $h . ', server hmac=' . $hmac); + sendResp(S_BAD_SIGNATURE, $myLog, $apiKey); + exit; + } +} + +/* We need to add necessary parameters not available at earlier protocols after signature is computed. + */ +if ($protocol_version<2.0) { + /* We need to create a nonce manually here */ + $nonce = md5(uniqid(rand())); + $myLog->log(LOG_INFO, 'protocol version below 2.0. Created nonce ' . $nonce); + } + +//// Which YK-KSM should we talk to? +// +$urls = otp2ksmurls ($otp, $client); +if (!is_array($urls)) { + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; +} + +//// Decode OTP from input +// +$otpinfo = KSMdecryptOTP($urls, $myLog); +if (!is_array($otpinfo)) { + sendResp(S_BAD_OTP, $myLog, $apiKey); + exit; +} +$myLog->log(LOG_DEBUG, "Decrypted OTP:", $otpinfo); + +//// Get Yubikey from DB +// +$devId = substr($otp, 0, strlen ($otp) - TOKEN_LEN); +$yk_publicname=$devId; +$localParams = $sync->getLocalParams($yk_publicname); +if (!$localParams) { + $myLog->log(LOG_NOTICE, 'Invalid Yubikey ' . $yk_publicname); + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; + } + +$myLog->log(LOG_DEBUG, "Auth data:", $localParams); +if ($localParams['active'] != 1) { + $myLog->log(LOG_NOTICE, 'De-activated Yubikey ' . $devId); + sendResp(S_BAD_OTP, $myLog, $apiKey); + exit; +} + +/* Build OTP params */ + +$otpParams=array('modified'=>time(), + 'otp'=>$otp, + 'nonce'=>$nonce, + 'yk_publicname'=>$devId, + 'yk_counter'=>$otpinfo['session_counter'], + 'yk_use'=>$otpinfo['session_use'], + 'yk_high'=>$otpinfo['high'], + 'yk_low'=>$otpinfo['low']); + + +/* First check if OTP is seen with the same nonce, in such case we have an replayed request */ +if ($sync->countersEqual($localParams, $otpParams) && + $localParams['nonce']==$otpParams['nonce']) { + $myLog->log(LOG_WARNING, 'Replayed request'); + sendResp(S_REPLAYED_REQUEST, $myLog, $apiKey, $extra); + exit; + } + +/* Check the OTP counters against local db */ +if ($sync->countersHigherThanOrEqual($localParams, $otpParams)) { + $sync->log(LOG_WARNING, 'replayed OTP: Local counters higher'); + $sync->log(LOG_WARNING, 'replayed OTP: Local counters ', $localParams); + $sync->log(LOG_WARNING, 'replayed OTP: Otp counters ', $otpParams); + sendResp(S_REPLAYED_OTP, $myLog, $apiKey, $extra); + exit; + } + +/* Valid OTP, update database. */ + +if(!$sync->updateDbCounters($otpParams)) { + $myLog->log(LOG_CRIT, "Failed to update yubikey counters in database"); + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; + } + +/* Queue sync requests */ + +if (!$sync->queue($otpParams, $localParams)) { + $myLog->log(LOG_CRIT, "ykval-verify:critical:failed to queue sync requests"); + sendResp(S_BACKEND_ERROR, $myLog, $apiKey); + exit; + } + +$nr_servers=$sync->getNumberOfServers(); +$req_answers=ceil($nr_servers*$sl/100.0); +if ($req_answers>0) { + $syncres=$sync->sync($req_answers, $timeout); + $nr_answers=$sync->getNumberOfAnswers(); + $nr_valid_answers=$sync->getNumberOfValidAnswers(); + $sl_success_rate=floor(100.0 * $nr_valid_answers / $nr_servers); + + } else { + $syncres=true; + $nr_answers=0; + $nr_valid_answers=0; + $sl_success_rate=0; + } +$myLog->log(LOG_INFO, "ykval-verify:notice:synclevel=" . $sl . + " nr servers=" . $nr_servers . + " req answers=" . $req_answers . + " answers=" . $nr_answers . + " valid answers=" . $nr_valid_answers . + " sl success rate=" . $sl_success_rate . + " timeout=" . $timeout); + +if($syncres==False) { + /* sync returned false, indicating that + either at least 1 answer marked OTP as invalid or + there were not enough answers */ + $myLog->log(LOG_WARNING, "ykval-verify:notice:Sync failed"); + if ($nr_valid_answers!=$nr_answers) { + sendResp(S_REPLAYED_OTP, $myLog, $apiKey, $extra); + exit; + } else { + $extra['sl']=$sl_success_rate; + sendResp(S_NOT_ENOUGH_ANSWERS, $myLog, $apiKey, $extra); + exit; + } + } + +/* Recreate parameters to make phising test work out + TODO: use timefunctionality in deltatime library instead */ +$sessionCounter = $otpParams['yk_counter']; +$sessionUse = $otpParams['yk_use']; +$seenSessionCounter = $localParams['yk_counter']; +$seenSessionUse = $localParams['yk_use']; + +$ad['high']=$localParams['yk_high']; +$ad['low']=$localParams['yk_low']; +$ad['accessed']=$sync->unixToDbTime($localParams['modified']); + +//// Check the time stamp +// +if ($sessionCounter == $seenSessionCounter && $sessionUse > $seenSessionUse) { + $ts = ($otpinfo['high'] << 16) + $otpinfo['low']; + $seenTs = ($ad['high'] << 16) + $ad['low']; + $tsDiff = $ts - $seenTs; + $tsDelta = $tsDiff * TS_SEC; + + //// Check the real time + // + $lastTime = strtotime($ad['accessed']); + $now = time(); + $elapsed = $now - $lastTime; + $deviation = abs($elapsed - $tsDelta); + + // Time delta server might verify multiple OTPS in a row. In such case validation server doesn't + // have time to tick a whole second and we need to avoid division by zero. + if ($elapsed != 0) { + $percent = $deviation/$elapsed; + } else { + $percent = 1; + } + $myLog->log(LOG_INFO, "Timestamp seen=" . $seenTs . " this=" . $ts . + " delta=" . $tsDiff . ' secs=' . $tsDelta . + ' accessed=' . $lastTime .' (' . $ad['accessed'] . ') now=' + . $now . ' (' . strftime("%Y-%m-%d %H:%M:%S", $now) + . ') elapsed=' . $elapsed . + ' deviation=' . $deviation . ' secs or '. + round(100*$percent) . '%'); + if ($deviation > TS_ABS_TOLERANCE && $percent > TS_REL_TOLERANCE) { + $myLog->log(LOG_NOTICE, "OTP failed phishing test"); + if (0) { + sendResp(S_DELAYED_OTP, $myLog, $apiKey, $extra); + exit; + } + } +} + +/* Fill up with more respone parameters */ +if ($protocol_version>=2.0) { + $extra['sl'] = $sl_success_rate; + } +if ($timestamp==1){ + $extra['timestamp'] = ($otpinfo['high'] << 16) + $otpinfo['low']; + $extra['sessioncounter'] = $sessionCounter; + $extra['sessionuse'] = $sessionUse; + } + +sendResp(S_OK, $myLog, $apiKey, $extra); + +?>