Added OpenSSH public key support in LDAP
authorDan Fuhry <dan@fuhry.us>
Fri, 11 Jan 2013 05:41:41 -0500
changeset 4 2212b2ded8bf
parent 3 a044870a9d3d
child 5 cdd708efa505
Added OpenSSH public key support in LDAP
make-sso
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/.htaccess
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/groups.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/functions.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/ldap.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/starthere.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/template-wrapper.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/index.tpl
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/my-account.tpl
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/index.php
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/base64.js
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/md5.js
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/ssoinabox.css
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/user-create-form.js
packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/users.php
resources/apache2-site.conf
resources/functions
resources/openssh-lpk_openldap.schema
--- a/make-sso	Fri Jan 11 00:32:54 2013 -0500
+++ b/make-sso	Fri Jan 11 05:41:41 2013 -0500
@@ -125,6 +125,9 @@
 if [ -d /etc/ldap/slapd.d ]; then
 	rm -rfv /etc/ldap/slapd.d
 fi
+
+cp `dirname $0`/resources/openssh-lpk_openldap.schema /etc/ldap/schema/
+
 generate_slapd_config
 generate_base_ldif | slapadd
 chown -R openldap:openldap /var/lib/ldap
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/.htaccess	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/.htaccess	Fri Jan 11 05:41:41 2013 -0500
@@ -1,9 +1,5 @@
-#AuthType Basic
-#AuthName "Accounts"
-#AuthUserFile /usr/local/share/ssoinabox/htdocs/.htpasswd
 AuthType WebAuth
-AuthDBMGroupFile /etc/apache2/ldap-groups
-Require group rtp
+Require valid-user
 
 <FilesMatch "^((webauth-)?logout|lostpw|pw-strength)(\.php)?$">
 	Require valid-user
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/groups.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/groups.php	Fri Jan 11 05:41:41 2013 -0500
@@ -1,5 +1,6 @@
 <?php
 
+define('NEED_ADMIN', 1);
 require('includes/starthere.php');
 
 // POSTed actions
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/functions.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/functions.php	Fri Jan 11 05:41:41 2013 -0500
@@ -26,6 +26,11 @@
 	return get_next_available_uid();
 }
 
+function smarty_function_json_encode($params)
+{
+	return json_encode($params['value']);
+}
+
 function load_credentials()
 {
 	$config = yaml_parse_file("/usr/local/etc/ssoinabox/webcreds.yml");
@@ -87,3 +92,49 @@
 	
 	return strlen($uniq);
 }
+
+$ssh_key_lengths = array(
+		// pubkey len => key bits
+		'ecdsa-sha2-nistp521' => array('name' => 'ECDSA', 172 => 521)
+		, 'ecdsa-sha2-nistp384' => array('name' => 'ECDSA', 136 => 384)
+		, 'ecdsa-sha2-nistp256' => array('name' => 'ECDSA', 104 => 256)
+		, 'ssh-dss' => array(
+				'name' => 'DSA'
+				, 432 => 1024
+				, 433 => 1024
+				, 434 => 1024
+				, 435 => 1024
+			)
+		, 'ssh-rsa' => array(
+				'name' => 'RSA'
+				, 119 => 768
+				, 151 => 1024
+				, 215 => 1536
+				, 277 => 2048
+				, 279 => 2048
+				, 407 => 3072
+				, 535 => 4096
+			)
+	);
+
+function smarty_function_decode_ssh_key($params, $smarty)
+{
+	global $ssh_key_lengths;
+	
+	if ( !isset($params['key']) )
+		throw new SmartyException("No key provided");
+	
+	if ( !isset($params['out']) )
+		throw new SmartyException("No output var provided");
+	
+	list($type, $key_b64) = preg_split('/\s+/', $params['key']);
+	
+	$key = base64_decode($key_b64);
+	$bits = isset($ssh_key_lengths[$type][strlen($key)]) ? $ssh_key_lengths[$type][strlen($key)] : 0;
+	
+	$smarty->assign($params['out'], array(
+			'fingerprint' => implode(':', str_split(md5($key), 2))
+			, 'type' => $ssh_key_lengths[$type]['name']
+			, 'bits' => $bits
+		));
+}
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/ldap.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/ldap.php	Fri Jan 11 05:41:41 2013 -0500
@@ -30,6 +30,7 @@
 		'telephoneNumber'
 		, 'mobile'
 		, 'mail'
+		, 'sshPublicKey'
 	);
 
 // END CONSTANTS
@@ -134,13 +135,15 @@
 
 function ldap_array_cleanup($arr)
 {
+	global $ldap_add_multiple;
+	
 	$result = array();
 	foreach ( $arr as $k => $v )
 	{
 		if ( is_int($k) || $k == 'count' )
 			continue;
 		
-		if ( $v['count'] === 1 )
+		if ( $v['count'] === 1 && !in_array($k, $ldap_add_multiple) )
 			$v = $v[0];
 		else
 			unset($v['count']);
@@ -304,6 +307,7 @@
 				, 'inetOrgPerson'
 				, 'organizationalPerson'
 				, 'posixAccount'
+				, 'ldapPublicKey'
 				)
 			, 'gn' => array($gn)
 			, 'sn' => array($sn)
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/starthere.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/starthere.php	Fri Jan 11 05:41:41 2013 -0500
@@ -22,5 +22,11 @@
 $adm = !empty($_SERVER['REMOTE_USER']) && ldap_test_group_membership($_SERVER['REMOTE_USER'], 'rtp');
 define('IS_ADMIN', $adm);
 
+if ( !IS_ADMIN && defined('NEED_ADMIN') )
+{
+	queue_message(E_ERROR, "Access to that URL is restricted to administrators.");
+	redirect('/');
+}
+
 if ( !isset($_SESSION['messages']) )
 	$_SESSION['messages'] = array();
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/template-wrapper.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/template-wrapper.php	Fri Jan 11 05:41:41 2013 -0500
@@ -26,6 +26,9 @@
 			, 'notice' => E_NOTICE
 		));
 	
+	global $ssh_key_lengths;
+	$smarty->assign('ssh_key_lengths', $ssh_key_lengths);
+	
 	foreach ( $assign as $key => $value )
 	{
 		$smarty->assign($key, $value);
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/index.tpl	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/index.tpl	Fri Jan 11 05:41:41 2013 -0500
@@ -3,13 +3,19 @@
 
 <h1>Welcome, {$userinfo['givenName']}</h1>
 
-<p>This console is used for management of user accounts and groups. You can create and delete accounts, reset passwords, manage group memberships, and
-	disable/enable accounts.</p>
+<p>This console provides you with the ability to update information about your account, change your password, manage your SSH keys, and more.</p>
+
+{if $is_admin}
+<p>Administrative capabilities of this console include management of user accounts, groups, and assets. You can create and delete accounts, reset
+	passwords, manage group memberships, and disable/enable accounts.</p>
 
 <div class="alert alert-error">
 	<strong>Security reminder:</strong><br />
 	The powers granted by this console are immense. It is crucial that you remove access to this console by logging out using the menu in the upper right-hand corner of this page
 	when you are finished or if you need to leave your workstation for any reason.
 </div>
+{/if}
+
+{include file="my-account.tpl"}
 
 {include file="footer.tpl"}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/includes/templates/my-account.tpl	Fri Jan 11 05:41:41 2013 -0500
@@ -0,0 +1,131 @@
+<h2>Account settings</h2>
+
+<script type="text/javascript">
+//<![CDATA[
+var ssh_key_lengths = {json_encode value=$ssh_key_lengths};
+//]]>
+</script>
+<script type="text/javascript" src="/res/md5.js"></script>
+<script type="text/javascript" src="/res/base64.js"></script>
+<script type="text/javascript" src="/res/user-create-form.js"></script>
+
+<form method="post" class="form-horizontal" name="userCreateForm">
+
+	<!-- Password change -->
+	<div class="control-group">
+		<label class="control-label">Change your password:</label>
+		<div class="controls">
+			<a class="btn btn-inverse" data-toggle="modal" href="#modal-pwchange">
+				<i class="icon icon-white icon-refresh"></i>
+				Change password
+			</a>
+		</div>
+	</div>
+	
+	<!-- E-mail address -->
+	<div class="control-group">
+		<label class="control-label">E-mail address:</label>
+		<div class="controls">
+			<input type="text" name="mail" value="{if isset($userinfo['mail'])}{$userinfo['mail']|escape:'html'}{/if}" />
+			<p>Enter an e-mail address if you want to be able to use password recovery.</p>
+		</div>
+	</div>
+	
+	<!-- SSH keys -->
+	<div class="control-group">
+		<label class="control-label">SSH keys:</label>
+		<div class="controls put-ssh-keys-here">
+			{if isset($userinfo['sshPublicKey'])}
+				{foreach $userinfo['sshPublicKey'] as $sshKey}
+					<div class="btn btn-warning ssh-key">
+						<div class="pull-right">
+							<a class="close">&times;</a>
+						</div>
+						{decode_ssh_key key=$sshKey out="decoded"}
+						<span class="label"><span class="type">{$decoded['type']}</span> <span class="bits">{$decoded['bits']}</span>-bit</span>
+						<span class="fingerprint">{$decoded['fingerprint']}</span>
+						<input type="hidden" name="sshPublicKey[]" value="{$sshKey}" />
+					</div>
+				{/foreach}
+			{/if}
+			
+			<a class="btn btn-success" data-toggle="modal" href="#modal-addssh">
+				<i class="icon icon-white icon-plus"></i>
+				Add SSH key
+			</a>
+		</div>
+	</div>
+	
+	<div class="form-actions">
+		<button name="action" value="profile-update" class="btn btn-primary">
+			Save changes
+		</button>
+	</div>
+	
+</form>
+
+<form method="post" class="form-horizontal" name="userResetForm">
+<div class="modal hide fade" id="modal-pwchange">
+	<div class="modal-header">
+		<h3>Change password</h3>
+	</div>
+	<div class="modal-body">
+		<p>It is best to log out of all websites and services before changing your password. </p>
+		
+		<div class="control-group unpadded-bottom">
+			<label class="control-label">Old password:</label>
+			<div class="controls">
+				<p><input type="password" name="old_password" value="" placeholder="Old password" /></p>
+			</div>
+		</div>
+		
+		<div class="control-group unpadded-bottom">
+			<label class="control-label">Password:</label>
+			<div class="controls">
+				<p><input type="password" name="password" value="" placeholder="Password" /></p>
+				<p class="help-block compliance-status">Must meet
+					<a onclick="window.open(this.href); return false;" href="/pw-strength">password security requirements</a>.</p>
+			</div>
+		</div>
+		
+		<div class="control-group">
+			<div class="controls">
+				<p><input type="password" name="password_confirm" value="" placeholder="Confirm password" /></p>
+				<p class="help-block compliance-status"></p>
+			</div>
+		</div>
+	</div>
+	<div class="modal-footer">
+		<button class="btn btn-inverse" name="action" value="change-password">
+			<i class="icon icon-white icon-refresh"></i>
+			Change password
+		</button>
+		<a data-dismiss="modal" class="btn">Cancel</a>
+	</div>
+</div>
+</form>
+
+<form method="post" class="form-horizontal" name="addSSHKey">
+<div class="modal hide fade" id="modal-addssh">
+	<div class="modal-header">
+		<h3>Add SSH key</h3>
+	</div>
+	<div class="modal-body">
+		<p>Adding an SSH key will enable the use of that key on servers which support LDAP based SSH keys.</p>
+		
+		<div class="control-group unpadded-bottom">
+			<label class="control-label">Paste key:</label>
+			<div class="controls">
+				<textarea class="span4" id="newSSHKey" rows="5"></textarea>
+			</div>
+		</div>
+	</div>
+	<div class="modal-footer">
+		<button class="btn btn-success" name="action" value="add-ssh-key">
+			<i class="icon icon-white icon-plus"></i>
+			Add key
+		</button>
+		<a data-dismiss="modal" class="btn">Cancel</a>
+	</div>
+</div>
+</form>
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/index.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/index.php	Fri Jan 11 05:41:41 2013 -0500
@@ -2,4 +2,62 @@
 
 require('includes/starthere.php');
 
+if ( isset($_POST['action']) )
+{
+	switch($_POST['action'])
+	{
+	case 'change-password':
+		try
+		{
+			// verify old password
+			$result = @ldap_bind($_ldapconn, ldap_make_user_dn($_SERVER['REMOTE_USER']), $_POST['old_password']);
+			if ( !$result )
+				throw new Exception("Your old password was incorrect.");
+			
+			if ( ($result = test_password($_POST['password'])) !== true )
+				throw new Exception("Your new password $result.");
+			
+			if ( $_POST['password'] !== $_POST['password_confirm'] )
+				throw new Exception("The passwords you entered did not match.");
+			
+			if ( reset_password($_SERVER['REMOTE_USER'], $_POST['password']) )
+			{
+				// rebind to LDAP as manager, since we did a bind to verify the old password
+				ldap_bind($_ldapconn, $ldap_manager['dn'], $ldap_manager['password']);
+				queue_message(E_NOTICE, "Your password has been changed.");
+				break;
+			}
+			else
+			{
+				throw new Exception("Internal error when performing password reset.");
+			}
+		}
+		catch ( Exception $e )
+		{
+			queue_message(E_ERROR, $e->getMessage());
+			
+			// rebind to LDAP as manager, since we did a bind to verify the old password
+			ldap_bind($_ldapconn, $ldap_manager['dn'], $ldap_manager['password']);
+		}
+		break;
+	case 'profile-update':
+		// header('Content-type: text/plain'); print_r(!empty($_POST['sshPublicKey']) ? $_POST['sshPublicKey'] : array()); exit;
+		$result = ldap_mod_replace($_ldapconn, ldap_make_user_dn($_SERVER['REMOTE_USER']), array(
+				'mail' => array($_POST['mail'])
+				, 'sshPublicKey' => !empty($_POST['sshPublicKey']) ? array_unique($_POST['sshPublicKey']) : array()
+			));
+		
+		if ( $result || ldap_error($_ldapconn) === 'Success' )
+		{
+			queue_message(E_NOTICE, "Your information has been updated.");
+			redirect('/');
+		}
+		else
+		{
+			queue_message(E_ERROR, ldap_error($_ldapconn));
+		}
+		break;
+	}
+}
+
 display_template('index');
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/base64.js	Fri Jan 11 05:41:41 2013 -0500
@@ -0,0 +1,85 @@
+var keyStr = "ABCDEFGHIJKLMNOP" +
+       "QRSTUVWXYZabcdef" +
+       "ghijklmnopqrstuv" +
+       "wxyz0123456789+/" +
+       "=";
+       
+var Base64 = {
+
+	encode: function (input) {
+			input = escape(input);
+			var output = "";
+			var chr1, chr2, chr3 = "";
+			var enc1, enc2, enc3, enc4 = "";
+			var i = 0;
+			
+			do {
+				chr1 = input.charCodeAt(i++);
+				chr2 = input.charCodeAt(i++);
+				chr3 = input.charCodeAt(i++);
+				
+				enc1 = chr1 >> 2;
+				enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+				enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+				enc4 = chr3 & 63;
+				
+				if (isNaN(chr2)) {
+					enc3 = enc4 = 64;
+				} else if (isNaN(chr3)) {
+					enc4 = 64;
+				}
+				
+				output = output +
+				   keyStr.charAt(enc1) +
+				   keyStr.charAt(enc2) +
+				   keyStr.charAt(enc3) +
+				   keyStr.charAt(enc4);
+				chr1 = chr2 = chr3 = "";
+				enc1 = enc2 = enc3 = enc4 = "";
+			} while (i < input.length);
+			
+			return output;
+		}
+
+	, decode : function (input) {
+			var output = "";
+			var chr1, chr2, chr3 = "";
+			var enc1, enc2, enc3, enc4 = "";
+			var i = 0;
+			
+			// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+			var base64test = /[^A-Za-z0-9\+\/\=]/g;
+			if (base64test.exec(input)) {
+			alert("There were invalid base64 characters in the input text.\n" +
+			      "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
+			      "Expect errors in decoding.");
+			}
+			input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+			
+			do {
+				enc1 = keyStr.indexOf(input.charAt(i++));
+				enc2 = keyStr.indexOf(input.charAt(i++));
+				enc3 = keyStr.indexOf(input.charAt(i++));
+				enc4 = keyStr.indexOf(input.charAt(i++));
+				
+				chr1 = (enc1 << 2) | (enc2 >> 4);
+				chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+				chr3 = ((enc3 & 3) << 6) | enc4;
+				
+				output = output + String.fromCharCode(chr1);
+				
+				if (enc3 != 64) {
+					output = output + String.fromCharCode(chr2);
+				}
+				if (enc4 != 64) {
+					output = output + String.fromCharCode(chr3);
+				}
+				
+				chr1 = chr2 = chr3 = "";
+				enc1 = enc2 = enc3 = enc4 = "";
+				
+			} while (i < input.length);
+			
+			return unescape(output);
+		}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/md5.js	Fri Jan 11 05:41:41 2013 -0500
@@ -0,0 +1,379 @@
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0;   /* hex output format. 0 - lowercase; 1 - uppercase        */
+var b64pad  = "";  /* base-64 pad character. "=" for strict RFC compliance   */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_md5(s)    { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
+function b64_md5(s)    { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
+function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
+function hex_hmac_md5(k, d)
+  { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function b64_hmac_md5(k, d)
+  { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function any_hmac_md5(k, d, e)
+  { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function md5_vm_test()
+{
+  return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
+}
+
+/*
+ * Calculate the MD5 of a raw string
+ */
+function rstr_md5(s)
+{
+  return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+}
+
+/*
+ * Calculate the HMAC-MD5, of a key and some data (raw strings)
+ */
+function rstr_hmac_md5(key, data)
+{
+  var bkey = rstr2binl(key);
+  if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
+
+  var ipad = Array(16), opad = Array(16);
+  for(var i = 0; i < 16; i++)
+  {
+    ipad[i] = bkey[i] ^ 0x36363636;
+    opad[i] = bkey[i] ^ 0x5C5C5C5C;
+  }
+
+  var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+  return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
+}
+
+/*
+ * Convert a raw string to a hex string
+ */
+function rstr2hex(input)
+{
+  try { hexcase } catch(e) { hexcase=0; }
+  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+  var output = "";
+  var x;
+  for(var i = 0; i < input.length; i++)
+  {
+    x = input.charCodeAt(i);
+    output += hex_tab.charAt((x >>> 4) & 0x0F)
+           +  hex_tab.charAt( x        & 0x0F);
+  }
+  return output;
+}
+
+/*
+ * Convert a raw string to a base-64 string
+ */
+function rstr2b64(input)
+{
+  try { b64pad } catch(e) { b64pad=''; }
+  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  var output = "";
+  var len = input.length;
+  for(var i = 0; i < len; i += 3)
+  {
+    var triplet = (input.charCodeAt(i) << 16)
+                | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
+                | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
+    for(var j = 0; j < 4; j++)
+    {
+      if(i * 8 + j * 6 > input.length * 8) output += b64pad;
+      else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
+    }
+  }
+  return output;
+}
+
+/*
+ * Convert a raw string to an arbitrary string encoding
+ */
+function rstr2any(input, encoding)
+{
+  var divisor = encoding.length;
+  var i, j, q, x, quotient;
+
+  /* Convert to an array of 16-bit big-endian values, forming the dividend */
+  var dividend = Array(Math.ceil(input.length / 2));
+  for(i = 0; i < dividend.length; i++)
+  {
+    dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
+  }
+
+  /*
+   * Repeatedly perform a long division. The binary array forms the dividend,
+   * the length of the encoding is the divisor. Once computed, the quotient
+   * forms the dividend for the next step. All remainders are stored for later
+   * use.
+   */
+  var full_length = Math.ceil(input.length * 8 /
+                                    (Math.log(encoding.length) / Math.log(2)));
+  var remainders = Array(full_length);
+  for(j = 0; j < full_length; j++)
+  {
+    quotient = Array();
+    x = 0;
+    for(i = 0; i < dividend.length; i++)
+    {
+      x = (x << 16) + dividend[i];
+      q = Math.floor(x / divisor);
+      x -= q * divisor;
+      if(quotient.length > 0 || q > 0)
+        quotient[quotient.length] = q;
+    }
+    remainders[j] = x;
+    dividend = quotient;
+  }
+
+  /* Convert the remainders to the output string */
+  var output = "";
+  for(i = remainders.length - 1; i >= 0; i--)
+    output += encoding.charAt(remainders[i]);
+
+  return output;
+}
+
+/*
+ * Encode a string as utf-8.
+ * For efficiency, this assumes the input is valid utf-16.
+ */
+function str2rstr_utf8(input)
+{
+  var output = "";
+  var i = -1;
+  var x, y;
+
+  while(++i < input.length)
+  {
+    /* Decode utf-16 surrogate pairs */
+    x = input.charCodeAt(i);
+    y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
+    if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
+    {
+      x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
+      i++;
+    }
+
+    /* Encode output as utf-8 */
+    if(x <= 0x7F)
+      output += String.fromCharCode(x);
+    else if(x <= 0x7FF)
+      output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
+                                    0x80 | ( x         & 0x3F));
+    else if(x <= 0xFFFF)
+      output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
+                                    0x80 | ((x >>> 6 ) & 0x3F),
+                                    0x80 | ( x         & 0x3F));
+    else if(x <= 0x1FFFFF)
+      output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
+                                    0x80 | ((x >>> 12) & 0x3F),
+                                    0x80 | ((x >>> 6 ) & 0x3F),
+                                    0x80 | ( x         & 0x3F));
+  }
+  return output;
+}
+
+/*
+ * Encode a string as utf-16
+ */
+function str2rstr_utf16le(input)
+{
+  var output = "";
+  for(var i = 0; i < input.length; i++)
+    output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
+                                  (input.charCodeAt(i) >>> 8) & 0xFF);
+  return output;
+}
+
+function str2rstr_utf16be(input)
+{
+  var output = "";
+  for(var i = 0; i < input.length; i++)
+    output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
+                                   input.charCodeAt(i)        & 0xFF);
+  return output;
+}
+
+/*
+ * Convert a raw string to an array of little-endian words
+ * Characters >255 have their high-byte silently ignored.
+ */
+function rstr2binl(input)
+{
+  var output = Array(input.length >> 2);
+  for(var i = 0; i < output.length; i++)
+    output[i] = 0;
+  for(var i = 0; i < input.length * 8; i += 8)
+    output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
+  return output;
+}
+
+/*
+ * Convert an array of little-endian words to a string
+ */
+function binl2rstr(input)
+{
+  var output = "";
+  for(var i = 0; i < input.length * 32; i += 8)
+    output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
+  return output;
+}
+
+/*
+ * Calculate the MD5 of an array of little-endian words, and a bit length.
+ */
+function binl_md5(x, len)
+{
+  /* append padding */
+  x[len >> 5] |= 0x80 << ((len) % 32);
+  x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+  var a =  1732584193;
+  var b = -271733879;
+  var c = -1732584194;
+  var d =  271733878;
+
+  for(var i = 0; i < x.length; i += 16)
+  {
+    var olda = a;
+    var oldb = b;
+    var oldc = c;
+    var oldd = d;
+
+    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
+    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
+    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
+    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
+    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
+
+    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
+    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
+    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
+    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
+    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
+    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
+    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
+    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
+    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
+    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
+    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
+    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
+    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
+    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
+    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
+    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+    a = safe_add(a, olda);
+    b = safe_add(b, oldb);
+    c = safe_add(c, oldc);
+    d = safe_add(d, oldd);
+  }
+  return Array(a, b, c, d);
+}
+
+/*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+function md5_cmn(q, a, b, x, s, t)
+{
+  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
+}
+function md5_ff(a, b, c, d, x, s, t)
+{
+  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+}
+function md5_gg(a, b, c, d, x, s, t)
+{
+  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+}
+function md5_hh(a, b, c, d, x, s, t)
+{
+  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+}
+function md5_ii(a, b, c, d, x, s, t)
+{
+  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+  return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function bit_rol(num, cnt)
+{
+  return (num << cnt) | (num >>> (32 - cnt));
+}
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/ssoinabox.css	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/ssoinabox.css	Fri Jan 11 05:41:41 2013 -0500
@@ -50,3 +50,9 @@
 div.control-group.unpadded-bottom {
 	margin-bottom: 2px;
 }
+
+div.btn.ssh-key {
+	width: 500px;
+	display: block;
+	margin-bottom: 10px;
+}
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/user-create-form.js	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/res/user-create-form.js	Fri Jan 11 05:41:41 2013 -0500
@@ -90,6 +90,8 @@
 				}
 			});
 		
+		setupSSHKeyUI();
+		
 		$('.show-tooltip').tooltip();
 	});
 
@@ -141,3 +143,115 @@
 		});
 	$('#userResetForm').modal('show');
 }
+
+function binToByteArray(str)
+{
+	var arr = [], str = escape(str);
+	
+	for ( var i = 0; i < str.length; i++ )
+	{
+		var chr = str.charAt(i);
+		switch(chr)
+		{
+		case '%':
+			if ( str.charAt(i+1) == 'u' && str.substr(i+2, 4).match(/[A-F0-9]{4}/) )
+			{
+				arr.push(parseInt(str.substr(i+2, 2), 16));
+				arr.push(parseInt(str.substr(i+4, 2), 16));
+				i += 4;
+				break;
+			}
+			else if ( str.substr(i+1, 2).match(/[A-F0-9]{2}/) )
+			{
+				arr.push(parseInt(str.substr(i+1, 2), 16));
+				i += 2;
+				break;
+			}
+		default:
+			arr.push(str.charCodeAt(i));
+		}
+	}
+	
+	return arr;
+}
+
+function setupSSHKeyUI()
+{
+	if ( typeof(document.forms.addSSHKey) !== 'object' )
+		return;
+	
+	$('div.btn.ssh-key a.close').click(function()
+		{
+			$(this).parents('div.btn.ssh-key').remove();
+		});
+	
+	$(document.forms.addSSHKey).submit(function()
+		{
+			$('#modal-addssh div.control-group').removeClass('error');
+			
+			var keyValue = $('#newSSHKey').val();
+			if ( (keyValue.split(/ /)).length < 2 )
+			{
+				$('#modal-addssh div.control-group').addClass('error');
+				return false;
+			}
+			
+			var keyValue = keyValue.split(/ /);
+			var keyType = keyValue[0];
+			var keyEncoded = keyValue[1];
+			
+			if ( typeof(ssh_key_lengths[keyType]) != 'object' )
+			{
+				$('#modal-addssh div.control-group').addClass('error');
+				return false;
+			}
+			
+			var keyLength = keyEncoded.length * 0.75 - keyEncoded.replace(/[^=]/g, '').length;
+			var keyBits = typeof(ssh_key_lengths[keyType][keyLength]) == 'number' ? ssh_key_lengths[keyType][keyLength] : 0;
+			var keyName = ssh_key_lengths[keyType].name;
+			var keyFingerprint = (rstr2hex(rstr_md5(Base64.decode(keyEncoded), 8))).split(/(..)/).join(':').replace(/(^:|:$)/g, '').replace(/::/g, ':');
+			
+			$('<div />')
+				.addClass('btn').addClass('btn-warning').addClass('ssh-key')
+				.append($('<div />')
+					.addClass('pull-right')
+					.append($('<a />')
+						.addClass('close')
+						.html('&times;')
+						.click(function()
+							{
+								$(this).parents('div.btn.ssh-key').remove();
+							})
+						)
+					)
+				.append($('<span />')
+					.addClass('label')
+					.append($('<span />')
+						.addClass('type')
+						.text(keyName)
+						)
+					.append(' ')
+					.append($('<span />')
+						.addClass('bits')
+						.text(keyBits)
+						)
+					.append('-bit')
+					)
+				.append(' ')
+				.append($('<span />')
+					.addClass('fingerprint')
+					.text(keyFingerprint)
+					)
+				.append($('<input />')
+					.attr('type', 'hidden')
+					.attr('name', 'sshPublicKey[]')
+					.val(keyType + ' ' + keyEncoded)
+					)
+				.prependTo('.put-ssh-keys-here');
+				
+			$('#newSSHKey').val('');
+			$('#modal-addssh').modal('hide');
+			
+			return false;
+		});
+}
--- a/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/users.php	Fri Jan 11 00:32:54 2013 -0500
+++ b/packages/ssoinabox-webui/root/usr/local/share/ssoinabox/htdocs/users.php	Fri Jan 11 05:41:41 2013 -0500
@@ -1,5 +1,6 @@
 <?php
 
+define('NEED_ADMIN', 1);
 require('includes/starthere.php');
 
 // POSTed actions
--- a/resources/apache2-site.conf	Fri Jan 11 00:32:54 2013 -0500
+++ b/resources/apache2-site.conf	Fri Jan 11 05:41:41 2013 -0500
@@ -27,7 +27,7 @@
 	</Location>
 
 	ScriptAlias /login "/usr/share/weblogin/login.fcgi"
-	ScriptAlias /logout "/usr/share/weblogin/logout.fcgi"
+	ScriptAlias /webauth-logout "/usr/share/weblogin/logout.fcgi"
 	ScriptAlias /pwchange "/usr/share/weblogin/pwchange.fcgi"
 	Alias /images "/usr/local/share/weblogin/ssoinabox/images/"
 	Alias /help.html "/usr/local/share/weblogin/ssoinabox/templates/help.html"
--- a/resources/functions	Fri Jan 11 00:32:54 2013 -0500
+++ b/resources/functions	Fri Jan 11 05:41:41 2013 -0500
@@ -54,6 +54,7 @@
 include         /etc/ldap/schema/cosine.schema
 include         /etc/ldap/schema/inetorgperson.schema
 include         /etc/ldap/schema/nis.schema
+include         /etc/ldap/schema/openssh-lpk_openldap.schema
 
 pidfile		/var/run/slapd/slapd.pid	
 argsfile	/var/run/slapd/slapd.args
@@ -150,6 +151,7 @@
 objectClass: inetOrgPerson
 objectClass: organizationalPerson
 objectClass: posixAccount
+objectClass: ldapPublicKey
 cn: $fullname
 givenName: $gn
 sn: $sn
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/resources/openssh-lpk_openldap.schema	Fri Jan 11 05:41:41 2013 -0500
@@ -0,0 +1,19 @@
+#
+# LDAP Public Key Patch schema for use with openssh-ldappubkey
+# Author: Eric AUGE <eau@phear.org>
+# 
+# Based on the proposal of : Mark Ruijter
+#
+
+
+# octetString SYNTAX
+attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' 
+	DESC 'MANDATORY: OpenSSH Public key' 
+	EQUALITY octetStringMatch
+	SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
+
+# printableString SYNTAX yes|no
+objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
+	DESC 'MANDATORY: OpenSSH LPK objectclass'
+	MAY ( sshPublicKey $ uid ) 
+	)