Last active
July 20, 2016 13:18
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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