Skip to content

Instantly share code, notes, and snippets.

@Zash
Last active July 20, 2016 13:18
Show Gist options
  • Save Zash/5946686 to your computer and use it in GitHub Desktop.
Save Zash/5946686 to your computer and use it in GitHub Desktop.
Riddim plugin for showing info about certificates along with server-side support plugin.
local xmlns = "http://zash.se/protocol/s2scertinfo";
local st = require"util.stanza";
-- local dump = require"myserialize".serialize;
local base64_encode = require"util.encodings".base64.encode;
local s_char, s_gsub = string.char, string.gsub;
local tonumber = tonumber;
local function unhexbyte(c) return s_char(tonumber(c, 16)) end
local function unhex(s) return s_gsub(s, "..", unhexbyte) end
local function hex2b64(s) return base64_encode(unhex(s)) end
local oid_aliases = {
["1.3.6.1.5.5.7.8.5"] = "xmppAddr";
["1.3.6.1.5.5.7.8.7"] = "sRVName";
};
local function escape_utf8 (s)
local push, join = table.insert, table.concat;
local r, i = {}, 1;
if not(s and #s > 0) then
return ""
end
while true do
local c = s:sub(i,i)
local b = c:byte();
local w = (
(b >= 9 and b <= 10 and 0) or
(b >= 32 and b <= 126 and 0) or
(b >= 192 and b <= 223 and 1) or
(b >= 224 and b <= 239 and 2) or
(b >= 240 and b <= 247 and 3) or
(b >= 248 and b <= 251 and 4) or
(b >= 251 and b <= 252 and 5) or nil
)
if not w then
push(r, ("\\%03d"):format(b));
else
local n = i + w;
if w == 0 then
push(r, c);
elseif n > #s then
push(r, ("\\%d"):format(b));
else
local e = s:sub(i+1,n);
if e:match('^[\128-\191]*$') then
push(r, c);
push(r, e);
i = n;
else
push(r, ("\\%03d"):format(b));
end
end
end
i = i + 1;
if i > #s then
break
end
end
return join(r);
end
local function add_rdns(st, rdn)
for i, item in ipairs(rdn) do
if item.name then
st:tag(item.name, { xmlns = item.oid and "urn:oid:"..item.oid }):text(escape_utf8(item.value)):up();
end
end
end
local pat = "^([JFMAONSD][ceupao][glptbvyncr]) ?(%d%d?) (%d%d):(%d%d):(%d%d) (%d%d%d%d) GMT$";
local months = {Jan=1,Feb=2,Mar=3,Apr=4,May=5,Jun=6,Jul=7,Aug=8,Sep=9,Oct=10,Nov=11,Dec=12};
local function x509_to_datetime(s)
local month, day, hour, min, sec, year = s:match(pat); month = months[month];
return (("%04d-%02d-%02dT%02d:%02d:%02dZ"):format(year, month, day, hour, min, sec));
end
module:hook("iq-get/host/"..xmlns..":query", function(event)
local stanza, origin = event.stanza, event.origin;
local to_host = stanza:get_child_text("query", xmlns);
local host_session = prosody.hosts[module.host].s2sout[to_host];
if not (host_session and host_session.secure) then
return origin.send(st.error_reply(stanza, "modify", "item-not-found"));
end
local reply = st.reply(stanza)
:tag("stream", { xmlns = xmlns; from = host_session.from_host; to = host_session.to_host; })
local conn = host_session.conn:socket();
if conn and conn.info then
reply:tag("cipher");
for k,v in pairs(conn:info()) do
reply:tag(k):tag(type(v)):text(tostring(v)):up():up();
end
reply:up();
end
reply:tag("cert-status", { identity = host_session.cert_identity_status; chain = host_session.cert_chain_status; });
if conn and conn.getpeercertificate then
local cert = conn:getpeercertificate();
reply:tag("hash", { xmlns="urn:xmpp:hashes:1"; algo = "sha-1" }):text(hex2b64(cert:digest"sha1")):up();
reply:tag("hash", { xmlns="urn:xmpp:hashes:1"; algo = "sha-256" }):text(hex2b64(cert:digest"sha256")):up();
if cert.pubkey then
local _, typ, siz = cert:pubkey();
reply:tag("pubkey", { type = typ, size = tostring(siz) }):up();
end
reply:tag("issuer"); add_rdns(reply, cert:issuer()); reply:up();
reply:tag("subject"); add_rdns(reply, cert:subject()); reply:up();
if not cert:validat(os.time()) then
reply:tag("expired"):up();
end
reply:tag("notbefore"):text(x509_to_datetime(cert:notbefore())):up();
reply:tag("notafter"):text(x509_to_datetime(cert:notafter())):up();
local exts = cert:extensions();
if exts then
for e, extv in pairs(exts) do
if type(extv) == "table" then
local xmlns = e:match"^%d" and "urn:oid:"..e or nil;
reply:tag("extension", { name = extv.name or e, xmlns = xmlns });
for i, exti in pairs(extv) do
if type(exti) == "table" then
local xmlns = e:match"^%d" and "urn:oid:"..e or nil;
xmlns = exti.name and exti.name:match"^%d" and "urn:oid:"..exti.name or xmlns;
local name = i:match("^%d") and oid_aliases[exti.name or i] or exti.name or i;
reply:tag(name or "x", { xmlns = xmlns })
for i = 1,#exti do
reply:tag(name or "v"):text(escape_utf8(exti[i])):up();
end
reply:up();
end
end
reply:up();
end
end
end
end
return origin.send(reply);
end);
local split_jid = require"util.jid".prepped_split;
local parse_datetime = require"util.datetime".parse;
local unb64 = require"mime".unb64;
local function host_jid(jid)
return select(2, split_jid(jid));
end
local timeunits = {"minute",60,"hour",3600,"day",86400,"week",604800,"month",2629746,"year",31556952,};
local function humantime(timediff)
local ret = {};
for i=#timeunits,2,-2 do
if timeunits[i] < timediff then
local n = math.floor(timediff / timeunits[i]);
if n > 0 and #ret < 2 then
ret[#ret+1] = ("%d %s%s"):format(n, timeunits[i-1], n ~= 1 and "s" or "");
timediff = timediff - n*timeunits[i];
end
end
end
return table.concat(ret, " and ")
end
function riddim.plugins.s2sinfo(bot)
bot.stream:add_plugin("ping");
bot:hook("commands/certinfo", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local cert_status = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status";
if cert_status then
local chain_valid = cert_status.attr.chain == "valid";
local identity = cert_status.attr.identity;
local expired = cert_status:get_child("expired");
local ret = { jid }
-- table.insert(ret, reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status/subject/{urn:oid:2.5.4.3}commonName#")
if identity == "valid" then
table.insert(ret, "has a valid certificate");
elseif identity == "invalid" then
table.insert(ret, "has a mismatched certificate");
elseif cert_status:find"issuer/{urn:oid:2.5.4.3}commonName#" == cert_status:find"subject/{urn:oid:2.5.4.3}commonName#" then
table.insert(ret, "has a self-signed certificate");
elseif expired then
local notafter = cert_status:get_child_text("notafter");
notafter = notafter and parse_datetime(notafter);
if notafter then
table.insert(ret, "has an certificate that expired");
table.insert(ret, humantime(os.time() - notafter));
table.insert(ret, "ago");
else
table.insert(ret, "has an expired certificate");
end
elseif not chain_valid then
table.insert(ret, "has an untrusted certificate");
end
table.insert(ret, "issued by");
table.insert(ret, cert_status:find"issuer/{urn:oid:2.5.4.3}commonName#" or "Unknown CA");
command:reply(table.concat(ret, " "));
else
local type, condition, text = reply:get_error();
command:reply("Error: "..(text or condition));
end
end);
elseif error.condition == "remote-server-not-found" then
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
else
command:reply("Error: "..(error.text or error.condition));
end
end);
return true;
end
end);
bot:hook("commands/expires", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local ret = { jid }
local cert_status = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status";
if cert_status then
local expired = cert_status:get_child("expired");
local notafter = cert_status:get_child_text("notafter");
notafter = notafter and parse_datetime(notafter);
if not notafter then
if expired then
table.insert(ret, "has an expired certificate");
else
table.insert(ret, "has a certificate that is currently valid");
end
else
table.insert(ret, "has a certificate that");
if expired then
table.insert(ret, "expired");
table.insert(ret, humantime(os.time() - notafter));
table.insert(ret, "ago");
else
table.insert(ret, "expires in ");
table.insert(ret, humantime(notafter - os.time()));
end
end
command:reply(table.concat(ret, " "));
else
local type, condition, text = reply:get_error();
command:reply("Error: "..(text or condition));
end
end);
else
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
end
end);
return true;
end
end);
bot:hook("commands/fingerprint", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local hash = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status/{urn:xmpp:hashes:1}";
local algo = hash and hash.attr.algo; algo = algo and algo:upper();
local data = hash and unb64(hash:get_text()):gsub(".", function(b) return (":%02X"):format(b:byte()) end):sub(2);
if algo and data then
command:reply(("%s has %s fingerprint %s"):format(jid, algo, data));
else
local type, condition, text = reply:get_error();
command:reply("Error: "..(text or condition or "no fingerprint found"));
end
end);
else
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
end
end);
return true;
end
end);
local shortNames = {
commonName = "CN";
givenName = "GN";
stateOrProvinceName = "ST";
localityName = "L";
organizationName = "O";
countryName = "C";
organizationalUnitName = "OU";
surname = "SN";
};
bot:hook("commands/certsubject", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local subject = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status/subject";
local subject_out = { "" };
local n = #subject_out + 1;
for _, child in ipairs(subject.tags) do
subject_out[n] = (shortNames[child.name] or child.name) .. "=" .. child:get_text();
n = n + 1;
end
command:reply(("certificate of %s issued to %s"):format(jid, table.concat(subject_out, "/")));
end);
else
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
end
end);
end
end);
bot:hook("commands/cipher", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local cipher = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cipher/cipher/string#";
if cipher then
command:reply(("Connection to %s uses cipher %s"):format(jid, cipher));
else
local type, condition, text = reply:get_error();
command:reply("Error: "..(text or condition or "no cipher info returned"));
end
end);
else
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
end
end);
return true;
end
end);
bot:hook("commands/key", function (command)
local jid = command.param;
local myhost = host_jid(bot.stream.jid);
jid = host_jid(jid);
if jid then
bot.stream:ping(jid, function (time, jid, error)
if time or error.condition ~= "remote-server-not-found" then
bot.stream:send_iq(verse.iq{to=myhost,type="get"}:query"http://zash.se/protocol/s2scertinfo":text(jid),
function(reply)
local pubkey = reply and reply.attr.type ~= "error" and reply:find"{http://zash.se/protocol/s2scertinfo}stream/cert-status/pubkey";
if pubkey then
command:reply(("%s has a %d-bit %s key"):format(jid, pubkey.attr.size or -1, tostring(pubkey.attr.type)));
else
local type, condition, text = reply:get_error();
command:reply("Error: "..(text or condition or "no key info returned"));
end
end);
else
command:reply("Host unreachable: "..(text or condition or "no cert info returned"));
end
end);
return true;
end
end);
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment