module(..., package.seeall)
require("html")
-- There are two options of how to allow users to specify the type of certificate they want - the request extensions
-- and the ca signing extensions. We have opted for making all requests look the same (same extensions) and defining
-- different ca sections for the different types of certificates. The ca section to use when signing the request is
-- actually stored in the request filename. The request filename is in the following format:
-- 'username'.'ca section name'.'common name'.csr
local packagename = "openssl"
local configfile = "/etc/ssl/openssl.cnf"
local requestdir = "/etc/ssl/req/"
local certdir = "/etc/ssl/cert/"
local openssldir = "/etc/ssl/"
-- Save the config in a variable so isn't loaded each and every time needed
local config = nil
-- list of request entries that can be edited
local distinguished_names = { {name="countryName", label="Country Name", short="C"},
{name="stateOrProvinceName", label="State Or Province Name", short="ST"},
{name="localityName", label="Locality Name", short="L"},
{name="organizationName", label="Organization Name", short="O"},
{name="organizationalUnitName", label="Organizational Unit Name", short="OU"},
{name="commonName", label="Common Name", short="CN"},
{name="emailAddress", label="e-mail Address"} }
-- list of entries that may be found in cert extensions section
local extensions = { "basicConstraints", "nsCertType", "nsComment", "keyUsage", "subjectKeyIdentifier",
"authorityKeyIdentifier", "subjectAltName", "issuerAltName" }
-- list of entries that must be found in ca section
local ca_mandatory_entries = { "new_certs_dir", "certificate", "private_key", "default_md", "database", "policy" }
-- Validate the values of distinguished names using the min/max found in the config file
local validate_distinguished_names = function(values)
config = config or getopts.getoptsfromfile(configfile)
local distinguished_name = config.req.distinguished_name or ""
local success = true
for i, name in ipairs(distinguished_names) do
local min = config[distinguished_name][name.name.."_min"] or config[distinguished_name]["0."..name.name.."_min"]
if min and values.value[name.name] and #values.value[name.name].value < tonumber(min) then
values.value[name.name].errtxt = "Value too short"
success = false
end
local max = config[distinguished_name][name.name.."_max"] or config[distinguished_name]["0."..name.name.."_max"]
if max and values.value[name.name] and #values.value[name.name].value > tonumber(max) then
values.value[name.name].errtxt = "Value too long"
success = false
end
end
return success, values
end
-- Write distinguished name defaults to config file
local write_distinguished_names = function(values)
local file = fs.read_file(configfile)
config = config or getopts.getoptsfromfile(file)
local distinguished_name = config.req.distinguished_name or ""
for i,name in ipairs(distinguished_names) do
wname = name.name.."_default"
if config[distinguished_name]["0."..name.name] then
wname = "0."..wname
end
if values.value[name.name] then
local a,b,c
a,b,c, file = getopts.setoptsinfile(file, distinguished_name, wname, values.value[name.name].value)
end
end
fs.write_file(configfile, file)
config = getopts.getoptsfromfile(file)
end
local create_subject_string = function(values)
local outstr = {}
for i,name in ipairs(distinguished_names) do
outstr[#outstr + 1] = (name.short or name.name) .. "=" .. values.value[name.name].value
end
return "/"..table.concat(outstr, "/")
end
-- Find the sections of the config file that define ca's (ca -name option)
local find_ca_sections = function()
config = config or getopts.getoptsfromfile(configfile)
local cert_types = {}
for section in pairs(config) do
local success = true
for i,entry in ipairs(ca_mandatory_entries) do
if not config[section][entry] then
success = false
break
end
end
if success then
cert_types[#cert_types + 1] = section
end
end
return cert_types
end
local handle_req_clientdata = function(clientdata, defaults)
-- Next, put the user values into the table
for name,value in pairs(clientdata) do
if defaults.value[name] then
defaults.value[name].value = value
end
end
-- Next, validate the values
local success
success, defaults = validate_distinguished_names(defaults)
local foundcert=false
for i,cert in ipairs(defaults.value.certtype.option) do
if defaults.value.certtype.value == cert then
foundcert=true
break
end
end
if not foundcert then
success = false
defaults.value.certtype.errtxt = "Invalid certificate type"
end
return success, defaults
end
local getconfigpath = function(section, value)
config = config or getopts.getoptsfromfile(configfile)
local result = config[section][value] or ""
while string.find(result, "%$[%w_]+") do
local sub = string.match(result, "%$[%w_]+")
result = string.gsub(result, sub, config[section][string.sub(sub,2)] or config[""][string.sub(sub,2)] or "")
end
return result
end
-- FIXME we need to make sure necessary files / directories / private key are there
verifyopenssl = function()
-- set the working directory once for model
posix.chdir(openssldir)
local retval = false
if fs.is_file(configfile) then
config = config or getopts.getoptsfromfile(configfile)
if config and config.ca and config.ca.default_ca then
local cacert_file = getconfigpath(config.ca.default_ca, "private_key")
if fs.is_file(cacert_file) then
retval=true
end
end
end
return cfe({ type="boolean", value=retval, label="openssl verified" })
end
getstatus = function()
require("processinfo")
posix.chdir(openssldir)
local value,errtxt=processinfo.package_version(packagename)
local version = cfe({ value=value, errtxt=errtxt, label="Program version" })
local conffile = cfe({ value=configfile, label="Configuration file" })
local cacert = cfe({ label="CA Certificate" })
local cacertcontents = cfe({ type="longtext", label="CA Certificate contents" })
if not fs.is_file(configfile) then
conffile.errtxt="File not found"
cacert.errtxt="File not defined"
cacertcontents.errtxt=""
else
config = config or getopts.getoptsfromfile(configfile)
if (not config) or (not config.ca) or (not config.ca.default_ca) then
conffile.errtxt="Invalid config file"
cacert.errtxt="File not defined"
cacertcontents.errtxt=""
else
--cacert.value = getconfigpath(config.ca.default_ca, "private_key")
cacert.value = getconfigpath(config.ca.default_ca, "certificate")
if not fs.is_file(cacert.value) then
cacert.errtxt="File not found"
else
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl x509 -in "..cacert.value.." -noout -text"
local f = io.popen(cmd)
cacertcontents.value = f:read("*a")
f:close()
end
end
end
return cfe({ type="group", value={version=version, conffile=conffile, cacert=cacert, cacertcontents=cacertcontents}, label="openssl status" })
end
getreqdefaults = function()
local defaults = cfe({ type="group", value={} })
config = config or getopts.getoptsfromfile(configfile)
local distinguished_name = config.req.distinguished_name or ""
-- Get the distinguished name defaults
for i, name in ipairs(distinguished_names) do
defaults.value[name.name] = cfe({ label=name.label,
value=config[distinguished_name][name.name .. "_default"]
or config[distinguished_name]["0."..name.name.."_default"] or "",
descr=config[distinguished_name][name.name] or config[distinguished_name]["0."..name.name] })
end
-- Add in the ca type default
defaults.value.certtype = cfe({ type="select", label="Certificate Type",
value=config.ca.default_ca, option=find_ca_sections() })
return defaults
end
setreqdefaults = function(clientdata)
-- First, get the defaults
config = config or getopts.getoptsfromfile(configfile)
local defaults = getreqdefaults()
-- Then, copy in user values and validate
local success, defaults = handle_req_clientdata(clientdata, defaults)
-- Finally, write the values to the config file
if success then
getopts.setoptsinfile(configfile, "ca", "default_ca", defaults.value.certtype.value)
config = nil
write_distinguished_names(defaults)
end
if not success then
defaults.errtxt = "Failed to set defaults"
end
return defaults
end
getnewrequest = function()
local values = getreqdefaults()
-- In addition to the request defaults, we need a password and confirmation
values.value.password = cfe({ label="Password" })
values.value.password_confirm = cfe({ label="Password confirmation" })
return values
end
submitrequest = function(clientdata, user)
-- First, get the defaults
local defaults = getnewrequest()
-- Then, copy in user values and validate
local success, defaults = handle_req_clientdata(clientdata, defaults)
-- Must have a common name
if #defaults.value.commonName.value == 0 then
defaults.value.commonName.errtxt = "Common Name cannot be blank"
success = false
end
-- Check validity of password
if #defaults.value.password.value < 4 then
defaults.value.password.errtxt = "Password too short"
success = false
end
if defaults.value.password.value ~= defaults.value.password_confirm.value then
defaults.value.password_confirm.errtxt = "You entered wrong password/confirmation"
success = false
end
if success then
-- FIXME check to make sure same certificate or request doesn't already exist
end
if success then
-- Submit the request
local reqname = requestdir..user.."."..defaults.value.certtype.value.."."..defaults.value.commonName.value
local subject = create_subject_string(defaults)
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl req -nodes -new -config "..configfile.." -keyout "..reqname..".pem -out "..reqname..".csr -subj '"..subject.."' 2>&1"
local f = io.popen(cmd)
local cmdresult = f:read("*a")
f:close()
defaults.descr = cmdresult
if fs.is_file(reqname..".csr") then
fs.write_file(reqname..".pwd", defaults.value.password.value)
end
end
if not success then
defaults.errtxt = "Failed to submit request"
end
return defaults
end
listrequests = function(user)
user = user or "*"
local list={}
local fh = io.popen('find ' .. requestdir .. ' -name "'..user..'.*.csr" -maxdepth 1')
for x in fh:lines() do
local name = basename(x,".csr")
local a,b,c = string.match(name, "([^%.]*)%.([^%.]*)%.([^%.]*)")
list[#list + 1] = {name=name, user=a, certtype=b, commonName=c}
end
return cfe({ type="list", value=list, label="List of pending requests" })
end
viewrequest = function(request)
local path = requestdir .. request
local cmd = "openssl req -in "..path..".csr -text -noout"
local f = io.popen(cmd)
local cmdresult = f:read("*a")
f:close()
local a,b,c = string.match(request, "([^%.]*)%.([^%.]*)%.([^%.]*)")
local request = cfe({ type="table", value={name=name, user=a, certtype=b, commonName=c, value=cmdresult}, label="Request" })
return request
end
approverequest = function(request)
local cmdresult = cfe({ value="Failed to approve request", label="Approve result" })
local path = requestdir .. request
if fs.is_file(path..".csr") then
-- Request file exists, so try to sign
local user,certtype,commonName = string.match(request, "([^%.]*)%.([^%.]*)%.([^%.]*)")
-- Add the serial number to the end of the cert file name
local serialpath = getconfigpath(certtype, "serial")
local serialfile = fs.read_file(openssldir..serialpath)
local serial = string.match(serialfile, "%x%x")
local certname = certdir..request.."."..serial
-- Now, sign the certificate
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl ca -config "..configfile.." -in "..path..".csr -out "..certname..".crt -name "..certtype.." -batch 2>&1"
local f = io.popen(cmd)
cmdresult.value = f:read("*a")
f:close()
-- If certificate created, create the wrapped up pkcs12
if fs.is_file(certname..".crt") then
cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl pkcs12 -export -inkey "..path..".pem -in "..certname..".crt -out "..certname..".pfx -passout file:"..path..".pwd 2>&1"
f = io.popen(cmd)
local newcmdresult = f:read("*a")
f:close()
cmdresult.value = cmdresult.value .. newcmdresult
end
-- Finally, remove the request
if fs.is_file(certname..".pfx") then
cmd = "rm "..path..".*"
f = io.popen(cmd)
f:close()
end
end
return cmdresult
end
deleterequest = function(request, user)
user = user or ".*"
if (not fs.is_file(requestdir..request..".csr")) or (not string.find(request, "^"..user.."%.")) then
return cfe({ value="Request not found", label="Delete result" })
end
cmd = "rm "..requestdir..request..".*"
f = io.popen(cmd)
f:close()
return cfe({ value="Request deleted", label="Delete result" })
end
listcerts = function(user)
user = user or "*"
local list={}
local fh = io.popen('find ' .. certdir .. ' -name "'..user..'.*.pfx" -maxdepth 1')
for x in fh:lines() do
local name = basename(x,".pfx")
local a,b,c,d = string.match(name, "([^%.]*)%.([^%.]*)%.([^%.]*).([^%.]*)")
list[#list + 1] = {name=name, user=a, certtype=b, commonName=c, serial=d}
end
fh:close()
return cfe({ type="list", value=list, label="List of approved certificates" })
end
viewcert = function(cert)
local cmdresult = fs.read_file(certdir..cert..".crt")
local a,b,c,d = string.match(cert, "([^%.]*)%.([^%.]*)%.([^%.]*).([^%.]*)")
return cfe({ type="table", value={name=name, user=a, certtype=b, commonName=c, serial=d, value=cmdresult}, label="Certificate" })
end
getcert = function(cert)
local f = fs.read_file(certdir..cert..".pfx")
return cfe({ type="raw", value=f, label=cert..".pfx", option="application/x-pkcs12" })
--return cfe({ type="raw", value=f, label=cert..".pfx" })
end
revokecert = function(cert)
local cmdresult = cfe({ label="Revoke result" })
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl ca -config "..configfile.." -revoke "..certdir .. cert..".crt -batch 2>&1"
local f = io.popen(cmd)
cmdresult.value = f:read("*a")
f:close()
return cmdresult
end
deletecert = function(cert)
-- The certificate will still be in the ca directories and index.txt, just not available for web interface
cmd = "rm "..certdir..cert..".*"
f = io.popen(cmd)
f:close()
return cfe({ value="Certificate deleted", label="Delete result" })
end
listrevoked = function()
config = config or getopts.getoptsfromfile(configfile)
local databasepath = getconfigpath(config.ca.default_ca, "database")
local revoked = {}
local database = fs.read_file_as_array(databasepath)
for x,line in ipairs(database) do
if string.sub(line,1,1) == "R" then
revoked[#revoked + 1] = string.match(line, "^%S+%s+%S+%s+%S+%s+(%S+)")
end
end
return cfe({ type="list", value=revoked, label="Revoked serial numbers" })
end
getcrl = function(crltype)
local crlfile = cfe({ type="raw", label="Revoke list", option="application/pkix-crl" })
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl ca -config "..configfile.." -gencrl -out "..openssldir.."ca-crl.crl"
local f = io.popen(cmd)
f:close()
local cmd = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl crl -in "..openssldir.."ca-crl.crl -out "..openssldir.."ca-der-crl.crl -outform DER"
local f = io.popen(cmd)
f:close()
if crltype == "DER" then
crlfile.label = "ca-der-crl.crl"
crlfile.value = fs.read_file(crlfile.label)
elseif crltype == "PEM" then
crlfile.label = "ca-crl.crl"
crlfile.value = fs.read_file(crlfile.label)
else
crlfile.value = fs.read_file("ca-crl.crl")
end
return crlfile
end
-- FIXME this won't work because haserl doesn't support file upload. Untested and unfinished
putca = function(file, pword, set)
local ca = cfe({ type="raw", value=0, label="CA Certificate", descr='File must be a password protected ".pfx" file' })
local password = cfe({ label="Certificate Password" })
local retval = cfe({ type="group", value={ca=ca, password=password} })
if file and pword and set then
fs.write_file(openssldir.."temp.pfx", file)
fs.write_file(openssldir.."temp.pwd", pword)
-- Still need to verify input (using openssl pkcs12) and put cert and key in right place
end
return retval
end
getconfigfile = function()
local filename = cfe({ value=configfile, label="File Name" })
local filecontent = cfe({ type="longtext", label="Config file" })
local filesize = cfe({ value="0", label="File size" })
local mtime = cfe({ value="---", label="File date" })
if fs.is_file(configfile) then
local filedetails = fs.stat(configfile)
filecontent.value=fs.read_file(configfile)
filesize.value = filedetails.size
mtime.value = filedetails.mtime
else
filename.errtxt = "File not found"
end
return cfe({ type="group", value={filename=filename, filecontent=filecontent, filesize=filesize, mtime=mtime}, label="Config file details" })
end
setconfigfile = function(file)
if file and type(file)=="string" and #file>0 then
fs.write_file(configfile, file)
return true
end
return false
end