Public Key's not updating automatically

Public Key's not updating automatically

by Michael | -
Number of replies: 9
I noticed that the Public key's do not get automatically updated. It seemed like this should be in the auth/mnet cron but it wasn't.

So I added some code that now seems to do the job of updating the key's if they don't match.

Can anyone tell me if there is any problem for doing this here and with this code. It 'seems' like it should be ok to, but I am not a moodle/networking expert, so any comments are appreciated.

Here is the code added to auth/mnet/auth.php cron function

global $CFG;
require_once($CFG->libdir.'/adminlib.php');
include_once($CFG->dirroot.'/mnet/lib.php');

//update key
$hosts = get_records_select('mnet_host', " id != '{$CFG->mnet_localhost_id}' AND deleted = '0' ",'wwwroot ASC' );
foreach($hosts as $host) 
{
	if ($host->id != $CFG->mnet_all_hosts_id)
	{
		
		$hostid = $host->id;
		$mnet_peer = new mnet_peer();
		$mnet_peer->set_id($hostid);
		$currentkey = mnet_get_public_key($mnet_peer->wwwroot);
		if($currentkey != $mnet_peer->public_key)
		{
			//"key mismatch" update
			$mnet_peer->public_key = $currentkey;
			$bool = $mnet_peer->commit();
			if ($bool) {
				//good to go
			} else {
				error('Invalid action parameter.', 'index.php');
			}
		}
	}
}
Average of ratings: -
In reply to Michael |

Re: Public Key's not updating automatically

by Nigel McNie -
Hi Michael - this is an incredibly insecure way to do key rotation. Key rotation is handled securely at the time a request is made when one or both of the keys have expired. We planned to add cron rotation also as more of a "backup" than anything else, but haven't done it yet.

I will say this again, to make sure people reading this thread see it- this is an incredibly insecure way to do key rotation.

Basically, the problem is that you're asking for the public key without taking any steps to guarantee that the reply is coming from the host you think it is. The existing code uses the "old" keys to ensure this.

Did you run into a particular problem that triggered you to look for a solution?
In reply to Nigel McNie

Re: Public Key's not updating automatically

by Michael | -
Thanks for the reply Nigel!

The problem is that every 28 days the key's expire and you have to manually update them otherwise users can't 'roam' while the keys don't match.

Can you explain more on the insecurity here a bit more...

The first part get's the information you have in your database regarding a peer:
$mnet_peer = new mnet_peer();
$mnet_peer->set_id($hostid);

No problem here, right(?) since you are reading data from your own database.

The next part get's the current public key of the wwwroot, which you just got from your database, and if they don't match it updates.

$currentkey = mnet_get_public_key($mnet_peer->wwwroot);

Doesn't the mnet_get_public_key function check the host? I am not familiar with curl so am not clear on what exactly the function is doing, but I see these two lines within that function:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

So that looks to me like it is verifing.

Again, I am not a curl persion so which part of the above am I mistaken on?



Thanks in advance!
M=







In reply to Michael |

Re: Public Key's not updating automatically

by Nigel McNie -
There's no problem with the first part.

The problem comes from the second part. You're asking curl to make a HTTP request to a host at hostname 'X' - an unencrypted, unsigned request. You have no proof whatsoever that the host at that hostname is the same as the host you used to communicate with, and anyone who is listening in on your connection can send you back a bogus reply claiming to be from that hostname, thus causing you to store the attacker's chosen public key.

The curl options are largely irrelevant. It is not an HTTPS connection that is being made between the hosts - it is a HTTP request that is signed and encrypted using openssl, which is slightly different.

Currently Moodle ensures security by sending a signed, encrypted messages back and forth using the older keys to make sure that the new key is retrieved securely.

If you're having problems with the key rotation, it's likely that you need to upgrade your Moodle installations. What version of Moodle are you running on each end?
In reply to Nigel McNie

Re: Public Key's not updating automatically

by Michael | -
Thank you for the details.

We are using 1.8.3 and 1.8.7 versions of moodle.

So are you saying that later versions of moodle will automatically and securely get the new key and store? Do you know where, codewise, that is done in the new moodle? Upgrading is not an option at this point but I can update the code.

Thanks!
M=
In reply to Michael |

Re: Public Key's not updating automatically

by Nigel McNie -
The code is spread throughout the mnet/ directory. I know it's supposed to work in Moodle 1.8, but you would be wise to upgrade both Moodles to the latest stable code - even if that is only upgrading as far as the latest 1.8 release.
In reply to Michael |

Re: Public Key's not updating automatically

by Peter Bulmer -
There's a good chance that the 1.8.3 is the problem in this equation. Like Nigel, I definitely recommend upgrading to the latest 1.8.

In a relatively obscure way, the key rotation is expected to happen just as one client makes a request to the other - I call this "Lazy key rotation" - in effect nobody bothers to rotate keys, or tell other peers about their key rotations until they absolutely have to.

If you're interested, don't look in the cron for the key rotation/exchange code (this is where a 'plan-ahead' implementation would be), look at the message creation/sending/receiving back/retrying process.

hth,
Pete.
In reply to Peter Bulmer

Re: Public Key's not updating automatically

by Valery Fremaux -

Hi Peter, Nigel...

we encountered also the same problem than Michael in our TAO/Pairformance implementation for Intel. The fact is that the key rotation did not run properly in our case on 1.9.3 codebase. There is moreover another case where the "just when necessary" key upgrading does not comply the need : when using, as we do, massive use of XML-RPC data transfers between platform, for data exchange servicing. When adding such services, you barely not use the auth hook that could renew those keys. Keys should be renewed essentially throiugh an explicit remote auth action of a remote user. We cannot wait for that event in our case. So we made such an automated renewal solution :

We implemented it in a local/mnetcron.php hook to not denature standard code (although we rely on an unevitable patch in mnet/lib.php) :

<?php

/// check keys and reniew with peers.

/// requires : patching /mnet/xmlrpc/server.php for mnet_keyswap()
/// requires : patching /mnet/lib.php for mnet_keyswap()

global $MNET;

mtrace("Cron automatic rotation for MNET keys...\n");

if (!isset($CFG->mnet_key_autorenew_gap)) set_config('mnet_key_autorenew_gap', 24 * 3); // three days

// if autorenewal is enabled and we are mnetworking
if (!empty($CFG->mnet_key_autorenew) && $CFG->mnet_dispatcher_mode != 'none'){

    include_once $CFG->dirroot."/mnet/peer.php";
    include_once $CFG->dirroot."/mnet/lib.php";

    // check if key is getting obsolete
    $havetorenew = 0;

    // key is getting old : check if it is time to operate
    if ($MNET->public_key_expires - time() < $CFG->mnet_key_autorenew_gap * HOURSECS){

        // this one is needed as temporary global toggle between distinct cron invocations,
        // but should not be changed through the GUI
        if (empty($CFG->mnet_autorenew_haveto)){
            set_config('mnet_autorenew_haveto', 1);
            mtrace('Local key is expiring. Need renewing MNET keys...');
        } else {

            if (!empty($CFG->mnet_key_autorenew_time)){
                $now = getdate(time());
                if ($now['hours'] > $CFG->mnet_key_autorenew_time){
                    $havetorenew = 1;
                }
            } else {
                $havetorenew = 1;
            }
        }
    }

    // renew if needed
    $force = optional_param('forcerenew', 0, PARAM_INT);
   
    if ($havetorenew || $force){
        mtrace("Local key will expire very soon. Renew MNET keys now !!...\n");
        // make a key and exchange it with all known and active peers
        $mnet_peers = get_records('mnet_host', 'deleted', 0);
       
        // reniew local key
        $MNET->replace_keys();
       
        // send new key using key exchange transportation
        if ($mnet_peers){
            foreach($mnet_peers as $peer){

                if (($peer->id == $CFG->mnet_all_hosts_id) || ($peer->id == $CFG->mnet_localhost_id)) continue;

                $mnet_peer = new mnet_peer();
                $mnet_peer->set_wwwroot($peer->wwwroot);
                // get the sessions for each vmoodle that have same ID Number
                // we use a force parameter to force fetching the key remotely anyway
                $currentkey = mnet_get_public_key($mnet_peer->wwwroot, $mnet_peer->application, 1);
                if ($currentkey){
                    $mnet_peer->public_key = clean_param($currentkey, PARAM_PEM);
                    $mnet_peer->updateparams->public_key = clean_param($currentkey, PARAM_PEM);
                    $mnet_peer->public_key_expires = $mnet_peer->check_common_name($currentkey);
                    $mnet_peer->updateparams->public_key_expires = $mnet_peer->check_common_name($currentkey);
                    $mnet_peer->commit();
                    mtrace('Key renewed for '.$peer->wwwroot.' till '.userdate($mnet_peer->public_key_expires));
                } else {
                    mtrace('Failed renewing key with '.$peer->wwwroot);
                }
            }
        }      
        set_config('mnet_autorenew_haveto', 0);
    }
}

?>

This relies on a little change in get_public_key() and on the remote system/keyswap call that it implies  :

we added a force mode, that allows ofrcing renewal on certain conditions :

/**
 * Accepts a public key from a new remote host and returns the public key for
 * this host. If 'register all hosts' is turned on, it will bootstrap a record
 * for the remote host in the mnet_host table (if it's not already there)
 *
 * @param  string  $function      XML-RPC requires this but we don't... discard!
 * @param  array   $params        Array of parameters
 *                                $params[0] is the remote wwwroot
 *                                $params[1] is the remote public key
 * @return string                 The XML-RPC response
 */
function mnet_keyswap($function, $params) {
    global $CFG, $MNET;
    $return = array();

    $wwwroot        = $params[0];
    $pubkey         = $params[1];
    $application    = $params[2];
    $forcerenew     = $params[3];
   
    if ($forcerenew == 0){
        // standard keyswap for first key recording
        if (!empty($CFG->mnet_register_allhosts)) {
            $mnet_peer = new mnet_peer();
            $keyok = $mnet_peer->bootstrap($wwwroot, $pubkey, $application);
            if ($keyok) {
                $mnet_peer->commit();
            }
        }
    } else {
        $mnet_peer = new mnet_peer();
       
        // we can only renew hosts that we know something about.
        if ($mnet_peer->set_wwwroot($wwwroot)){
            $mnet_peer->public_key = clean_param($pubkey, PARAM_PEM);
            $mnet_peer->public_key_expires = $mnet_peer->check_common_name($pubkey);
            $mnet_peer->updateparams->public_key = clean_param($pubkey, PARAM_PEM);
            $mnet_peer->updateparams->public_key_expires = $mnet_peer->check_common_name($pubkey);
            $mnet_peer->commit();
        }
        return false; // avoid giving our key to unkown hosts.
       
    }
    return $MNET->public_key;
}

The precaution was not to let anyone ask for a key renewal, spoofing a registered host. I don't know if this precaution is sufficiant as it is. But it works in an internally secured subnetwork anyway.

The above change in renew process needs just passing an extra param in get_public_key (mnet/lib.php) :

/**
 * Get the remote machine's SSL Cert
 *
 * @param  string  $uri     The URI of a file on the remote computer, including
 *                          its http:// or https:// prefix
 * @return string           A PEM formatted SSL Certificate.
 */
// PATCH Mnet automated key renewal : adding force
function mnet_get_public_key($uri, $application=null, $force=0) {
// \PATCH Mnet automated key renewal
    global $CFG, $MNET;
    // The key may be cached in the mnet_set_public_key function...
    // check this first

// PATCH Mnet automated key renewal
    // cache location of key must be bypassed when we need an automated renew.
    if (!$force){
        $key = mnet_set_public_key($uri);
        if ($key != false) {
            return $key;
        }
    }
// \PATCH Mnet automated key renewal

    if (empty($application)) {
        $application = get_record('mnet_application', 'name', 'moodle');
    }

// PATCH Mnet automated key renewal
    $rq = xmlrpc_encode_request('system/keyswap', array($CFG->wwwroot, $MNET->public_key, $application->name, $force), array("encoding" => "utf-8"));
// \PATCH Mnet automated key renewal
    $ch = curl_init($uri . $application->xmlrpc_server_url);
...

This resolved all our issues of failure in standard key renewal, including those we would have expected to be working before the change.

Cheers.  

In reply to Valery Fremaux

Re: Public Key's not updating automatically

by Nigel McNie -
Hi Valery - things have been a bit slow here given Christmas etc. but I'll prod this in the direction of PeterB today - have you made a bug for this on the tracker? It might make the most sense to discuss it there.
In reply to Nigel McNie

Re: Public Key's not updating automatically

by Valery Fremaux -
Hi Nigel. I agree. I will check and open one if necessary.