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