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 configfile = "/etc/ssl/openssl.cnf"
local requestdir = "/etc/ssl/req/"
local certdir = "/etc/ssl/cert/"
local openssldir = "/etc/ssl/"
-- 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, inputconfig)
local config = inputconfig 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, inputconfig)
local file = fs.read_file(configfile)
local config = inputconfig 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)
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(inputconfig)
local config = inputconfig 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, config)
-- 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, config)
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, config
end
local getconfigpath = function(config, section, value)
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)])
end
return result
end
-- FIXME we need to make sure necessary files / directories / private key are there
verifyopenssl = function()
local retval = false
if fs.is_file(configfile) then
retval=true
end
return retval
end
getreqdefaults = function(inputconfig)
local defaults = cfe({ type="group", value={} })
local config = inputconfig 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(config) })
return defaults
end
setreqdefaults = function(clientdata)
-- First, get the defaults
local config = getopts.getoptsfromfile(configfile)
local defaults = getreqdefaults()
-- Then, copy in user values and validate
local success, defaults, config = handle_req_clientdata(clientdata, defaults, config)
-- Finally, write the values to the config file
if success then
write_distinguished_names(defaults, config)
getopts.setoptsinfile(configfile, "ca", "default_ca", defaults.value.certtype.value)
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 config = getopts.getoptsfromfile(configfile)
local defaults = getnewrequest(config)
-- Then, copy in user values and validate
local success, defaults, config = handle_req_clientdata(clientdata, defaults, config)
-- 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 config = getopts.getoptsfromfile(configfile)
local serialpath = getconfigpath(config, 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 cwd = posix.getcwd()
posix.chdir(openssldir)
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"
APP.logevent(cmd)
local f = io.popen(cmd)
cmdresult.value = f:read("*a")
f:close()
posix.chdir(cwd)
-- 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)
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
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" })
end
revokecert = function(cert)
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