mod_client_certs/mod_client_certs.lua
changeset 713 88ef66a65b13
parent 712 227d48f927ff
child 990 17ba2c59d661
equal deleted inserted replaced
712:227d48f927ff 713:88ef66a65b13
    15 local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5";
    15 local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5";
    16 local id_ce_subjectAltName = "2.5.29.17";
    16 local id_ce_subjectAltName = "2.5.29.17";
    17 local digest_algo = "sha1";
    17 local digest_algo = "sha1";
    18 local base64 = require "util.encodings".base64;
    18 local base64 = require "util.encodings".base64;
    19 
    19 
    20 local function enable_cert(username, cert, info)
       
    21 	local certs = dm_load(username, module.host, dm_table) or {};
       
    22 
       
    23 	info.pem = cert:pem();
       
    24 	local digest = cert:digest(digest_algo);
       
    25 	info.digest = digest;
       
    26 	certs[info.id] = info;
       
    27 
       
    28 	dm_store(username, module.host, dm_table, certs);
       
    29 	return true
       
    30 end
       
    31 
       
    32 local function disable_cert(username, name)
       
    33 	local certs = dm_load(username, module.host, dm_table) or {};
       
    34 
       
    35 	local info = certs[name];
       
    36 	local cert;
       
    37 	if info then
       
    38 		certs[name] = nil;
       
    39 		cert = x509.cert_from_pem(info.pem);
       
    40 	else
       
    41 		return nil, "item-not-found"
       
    42 	end
       
    43 
       
    44 	dm_store(username, module.host, dm_table, certs);
       
    45 	return cert; -- So we can compare it with stuff
       
    46 end
       
    47 
       
    48 local function get_id_on_xmpp_addrs(cert)
    20 local function get_id_on_xmpp_addrs(cert)
    49 	local id_on_xmppAddrs = {};
    21 	local id_on_xmppAddrs = {};
    50 	for k,ext in pairs(cert:extensions()) do
    22 	for k,ext in pairs(cert:extensions()) do
    51 		if k == id_ce_subjectAltName then
    23 		if k == id_ce_subjectAltName then
    52 			for e,extv in pairs(ext) do
    24 			for e,extv in pairs(ext) do
    59 		end
    31 		end
    60 	end
    32 	end
    61 	module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", "));
    33 	module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", "));
    62 	return id_on_xmppAddrs;
    34 	return id_on_xmppAddrs;
    63 end
    35 end
    64 	
    36 
       
    37 local function enable_cert(username, cert, info)
       
    38 	-- Check the certificate. Is it not expired? Does it include id-on-xmppAddr?
       
    39 
       
    40 	--[[ the method expired doesn't exist in luasec .. yet?
       
    41 	if cert:expired() then
       
    42 	module:log("debug", "This certificate is already expired.");
       
    43 	return nil, "This certificate is expired.";
       
    44 	end
       
    45 	--]]
       
    46 
       
    47 	if not cert:valid_at(os.time()) then
       
    48 		module:log("debug", "This certificate is not valid at this moment.");
       
    49 	end
       
    50 
       
    51 	local valid_id_on_xmppAddrs;
       
    52 	local require_id_on_xmppAddr = true;
       
    53 	if require_id_on_xmppAddr then
       
    54 		valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
       
    55 
       
    56 		local found = false;
       
    57 		for i,k in pairs(valid_id_on_xmppAddrs) do
       
    58 			if jid_bare(k) == (username .. "@" .. module.host) then
       
    59 				found = true;
       
    60 				break;
       
    61 			end
       
    62 		end
       
    63 
       
    64 		if not found then
       
    65 			return nil, "This certificate is has no valid id-on-xmppAddr field.";
       
    66 		end
       
    67 	end
       
    68 
       
    69 	local certs = dm_load(username, module.host, dm_table) or {};
       
    70 
       
    71 	info.pem = cert:pem();
       
    72 	local digest = cert:digest(digest_algo);
       
    73 	info.digest = digest;
       
    74 	certs[info.id] = info;
       
    75 
       
    76 	dm_store(username, module.host, dm_table, certs);
       
    77 	return true
       
    78 end
       
    79 
       
    80 local function disable_cert(username, name, disconnect)
       
    81 	local certs = dm_load(username, module.host, dm_table) or {};
       
    82 
       
    83 	local info = certs[name];
       
    84 
       
    85 	if not info then
       
    86 		return nil, "item-not-found"
       
    87 	end
       
    88 
       
    89 	certs[name] = nil;
       
    90 
       
    91 	if disconnect then
       
    92 		module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", username);
       
    93 		local sessions = hosts[module.host].sessions[username].sessions;
       
    94 		local disabled_cert_pem = info.pem;
       
    95 
       
    96 		for _, session in pairs(sessions) do
       
    97 			if session and session.conn then
       
    98 				local cert = session.conn:socket():getpeercertificate();
       
    99 
       
   100 				if cert and cert:pem() == disabled_cert_pem then
       
   101 					module:log("debug", "Found a session that should be closed: %s", tostring(session));
       
   102 					session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
       
   103 				end
       
   104 			end
       
   105 		end
       
   106 	end
       
   107 
       
   108 	dm_store(username, module.host, dm_table, certs);
       
   109 	return info;
       
   110 end
    65 
   111 
    66 module:hook("iq/self/"..xmlns_saslcert..":items", function(event)
   112 module:hook("iq/self/"..xmlns_saslcert..":items", function(event)
    67 	local origin, stanza = event.origin, event.stanza;
   113 	local origin, stanza = event.origin, event.stanza;
    68 	if stanza.attr.type == "get" then
   114 	if stanza.attr.type == "get" then
    69 		module:log("debug", "%s requested items", origin.full_jid);
   115 		module:log("debug", "%s requested items", origin.full_jid);
   117 		if not cert then
   163 		if not cert then
   118 			origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate"));
   164 			origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate"));
   119 			return true;
   165 			return true;
   120 		end
   166 		end
   121 
   167 
   122 		-- Check the certificate. Is it not expired? Does it include id-on-xmppAddr?
   168 		local ok, err = enable_cert(origin.username, cert, {
   123 
       
   124 		--[[ the method expired doesn't exist in luasec .. yet?
       
   125 		if cert:expired() then
       
   126 			module:log("debug", "This certificate is already expired.");
       
   127 			origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired."));
       
   128 			return true
       
   129 		end
       
   130 		--]]
       
   131 
       
   132 		if not cert:valid_at(os.time()) then
       
   133 			module:log("debug", "This certificate is not valid at this moment.");
       
   134 		end
       
   135 
       
   136 		local valid_id_on_xmppAddrs;
       
   137 		local require_id_on_xmppAddr = true;
       
   138 		if require_id_on_xmppAddr then
       
   139 			valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
       
   140 
       
   141 			local found = false;
       
   142 			for i,k in pairs(valid_id_on_xmppAddrs) do
       
   143 				if jid_bare(k) == jid_bare(origin.full_jid) then
       
   144 					found = true;
       
   145 					break;
       
   146 				end
       
   147 			end
       
   148 
       
   149 			if not found then
       
   150 				origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field."));
       
   151 				return true -- REJECT?!
       
   152 			end
       
   153 		end
       
   154 
       
   155 		enable_cert(origin.username, cert, {
       
   156 			id = id,
   169 			id = id,
   157 			name = name,
   170 			name = name,
   158 			x509cert = x509cert,
   171 			x509cert = x509cert,
   159 			no_cert_management = can_manage,
   172 			no_cert_management = can_manage,
   160 		});
   173 		});
   161 
   174 
       
   175 		if not ok then
       
   176 			origin.send(st.error_reply(stanza, "cancel", "bad-request", err));
       
   177 			return true -- REJECT?!
       
   178 		end
       
   179 
   162 		module:log("debug", "%s added certificate named %s", origin.full_jid, name);
   180 		module:log("debug", "%s added certificate named %s", origin.full_jid, name);
   163 
   181 
   164 		origin.send(st.reply(stanza));
   182 		origin.send(st.reply(stanza));
   165 
   183 
   166 		return true
   184 		return true
   180 		if not name then
   198 		if not name then
   181 			origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified."));
   199 			origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified."));
   182 			return true
   200 			return true
   183 		end
   201 		end
   184 
   202 
   185 		local disabled_cert = disable_cert(origin.username, name);
   203 		disable_cert(origin.username, name, disable.name == "revoke");
   186 
   204 
   187 		if disabled_cert and disable.name == "revoke" then
       
   188 			module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid);
       
   189 			local sessions = hosts[module.host].sessions[origin.username].sessions;
       
   190 			local disabled_cert_pem = disabled_cert:pem();
       
   191 
       
   192 			for _, session in pairs(sessions) do
       
   193 				if session and session.conn then
       
   194 					local cert = session.conn:socket():getpeercertificate();
       
   195 				
       
   196 					if cert and cert:pem() == disabled_cert_pem then
       
   197 						module:log("debug", "Found a session that should be closed: %s", tostring(session));
       
   198 						session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
       
   199 					end
       
   200 				end
       
   201 			end
       
   202 		end
       
   203 		origin.send(st.reply(stanza));
   205 		origin.send(st.reply(stanza));
   204 
   206 
   205 		return true
   207 		return true
   206 	end
   208 	end
   207 end
   209 end
   208 
   210 
   209 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable);
   211 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable);
   210 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable);
   212 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable);
       
   213 
       
   214 -- Ad-hoc command
       
   215 local adhoc_new = module:require "adhoc".new;
       
   216 local dataforms_new = require "util.dataforms".new;
       
   217 
       
   218 local function generate_error_message(errors)
       
   219 	local errmsg = {};
       
   220 	for name, err in pairs(errors) do
       
   221 		errmsg[#errmsg + 1] = name .. ": " .. err;
       
   222 	end
       
   223 	return table.concat(errmsg, "\n");
       
   224 end
       
   225 
       
   226 local choose_subcmd_layout = dataforms_new {
       
   227 	title = "Certificate management";
       
   228 	instructions = "What action do you want to perform?";
       
   229 
       
   230 	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#subcmd" };
       
   231 	{ name = "subcmd", type = "list-single", label = "Actions", required = true,
       
   232 		value = { {label = "Add certificate", value = "add"},
       
   233 			  {label = "List certificates", value = "list"},
       
   234 			  {label = "Disable certificate", value = "disable"},
       
   235 			  {label = "Revoke certificate", value = "revoke"},
       
   236 		};
       
   237 	};
       
   238 };
       
   239 
       
   240 local add_layout = dataforms_new {
       
   241 	title = "Adding a certificate";
       
   242 	instructions = "Enter the certificate in PEM format";
       
   243 
       
   244 	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#add" };
       
   245 	{ name = "name", type = "text-single", label = "Name", required = true };
       
   246 	{ name = "cert", type = "text-multi", label = "PEM certificate", required = true };
       
   247 	{ name = "manage", type = "boolean", label = "Can manage certificates", value = true };
       
   248 };
       
   249 
       
   250 
       
   251 local disable_layout_stub = dataforms_new { { name = "cert", type = "list-single", label = "Certificate", required = true } };
       
   252 
       
   253 
       
   254 local function adhoc_handler(self, data, state)
       
   255 	if data.action == "cancel" then return { status = "canceled" }; end
       
   256 
       
   257 	if not state or data.action == "prev" then
       
   258 		return { status = "executing", form = choose_subcmd_layout, actions = { "next" } }, {};
       
   259 	end
       
   260 
       
   261 	if not state.subcmd then
       
   262 		local fields, errors = choose_subcmd_layout:data(data.form);
       
   263 		if errors then
       
   264 			return { status = "completed", error = { message = generate_error_message(errors) } };
       
   265 		end
       
   266 		local subcmd = fields.subcmd
       
   267 
       
   268 		if subcmd == "add" then
       
   269 			return { status = "executing", form = add_layout, actions = { "prev", "next", "complete" } }, { subcmd = "add" };
       
   270 		elseif subcmd == "list" then
       
   271 			local list_layout = dataforms_new {
       
   272 				title = "List of certificates";
       
   273 			};
       
   274 
       
   275 			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
       
   276 
       
   277 			for digest, info in pairs(certs) do
       
   278 				list_layout[#list_layout + 1] = { name = info.id, type = "text-multi", label = info.name, value = info.x509cert };
       
   279 			end
       
   280 
       
   281 			return { status = "completed", result = list_layout };
       
   282 		else
       
   283 			local layout = dataforms_new {
       
   284 				{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#" .. subcmd };
       
   285 				{ name = "cert", type = "list-single", label = "Certificate", required = true };
       
   286 			};
       
   287 
       
   288 			if subcmd == "disable" then
       
   289 				layout.title = "Disabling a certificate";
       
   290 				layout.instructions = "Select the certificate to disable";
       
   291 			elseif subcmd == "revoke" then
       
   292 				layout.title = "Revoking a certificate";
       
   293 				layout.instructions = "Select the certificate to revoke";
       
   294 			end
       
   295 
       
   296 			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
       
   297 
       
   298 			local values = {};
       
   299 			for digest, info in pairs(certs) do
       
   300 				values[#values + 1] = { label = info.name, value = info.id };
       
   301 			end
       
   302 
       
   303 			return { status = "executing", form = { layout = layout, values = { cert = values } }, actions = { "prev", "next", "complete" } },
       
   304 				{ subcmd = subcmd };
       
   305 		end
       
   306 	end
       
   307 
       
   308 	if state.subcmd == "add" then
       
   309 		local fields, errors = add_layout:data(data.form);
       
   310 		if errors then
       
   311 			return { status = "completed", error = { message = generate_error_message(errors) } };
       
   312 		end
       
   313 
       
   314 		local name = fields.name;
       
   315 		local x509cert = fields.cert:gsub("^%s*(.-)%s*$", "%1");
       
   316 
       
   317 		local cert = x509.cert_from_pem(
       
   318 		"-----BEGIN CERTIFICATE-----\n"
       
   319 		.. x509cert ..
       
   320 		"\n-----END CERTIFICATE-----\n");
       
   321 
       
   322 		if not cert then
       
   323 			return { status = "completed", error = { message = "Could not parse X.509 certificate" } };
       
   324 		end
       
   325 
       
   326 		local ok, err = enable_cert(jid_split(data.from), cert, {
       
   327 			id = cert:digest(digest_algo),
       
   328 			name = name,
       
   329 			x509cert = x509cert,
       
   330 			no_cert_management = not fields.manage
       
   331 		});
       
   332 
       
   333 		if not ok then
       
   334 			return { status = "completed", error = { message = err } };
       
   335 		end
       
   336 
       
   337 		module:log("debug", "%s added certificate named %s", data.from, name);
       
   338 
       
   339 		return { status = "completed", info = "Successfully added certificate " .. name .. "." };
       
   340 	else
       
   341 		local fields, errors = disable_layout_stub:data(data.form);
       
   342 		if errors then
       
   343 			return { status = "completed", error = { message = generate_error_message(errors) } };
       
   344 		end
       
   345 
       
   346 		local info = disable_cert(jid_split(data.from), fields.cert, state.subcmd == "revoke" );
       
   347 
       
   348 		if state.subcmd == "revoke" then
       
   349 			return { status = "completed", info = "Revoked certificate " .. info.name .. "."  };
       
   350 		else
       
   351 			return { status = "completed", info = "Disabled certificate " .. info.name .. "."  };
       
   352 		end
       
   353 	end
       
   354 end
       
   355 
       
   356 local cmd_desc = adhoc_new("Manage certificates", "http://prosody.im/protocol/certs", adhoc_handler, "user");
       
   357 module:provides("adhoc", cmd_desc);
   211 
   358 
   212 -- Here comes the SASL EXTERNAL stuff
   359 -- Here comes the SASL EXTERNAL stuff
   213 
   360 
   214 local now = os.time;
   361 local now = os.time;
   215 module:hook("stream-features", function(event)
   362 module:hook("stream-features", function(event)