diff -r 71503088f51b -r f880f219c566 openpkg/license.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openpkg/license.lua Tue Jul 31 12:23:42 2012 +0200 @@ -0,0 +1,808 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +- -- +- -- OpenPKG Framework License Processor +- -- Copyright (c) 2000-2012 OpenPKG GmbH +- -- +- -- This software is property of the OpenPKG GmbH, DE MUC HRB 160208. +- -- All rights reserved. Licenses which grant limited permission to use, +- -- copy, modify and distribute this software are available from the +- -- OpenPKG GmbH. +- -- +- -- THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED +- -- WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +- -- MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +- -- IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR +- -- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +- -- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +- -- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +- -- USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +- -- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +- -- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +- -- OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +- -- SUCH DAMAGE. +- -- + +- -- This is the RPM run-time integrity processor of the OpenPKG +- -- Framework. It currently checks the OpenPKG Framework run-time +- -- license only. The following grammar specifies and documents all +- -- currently supported license parameters. +- -- +- -- license ::= "Assertion-MinProcVersion:" version +- -- # require a minimum version of the license integrity processor +- -- +- -- | "Assertion-ErrorToWarning:" yes-no +- -- # allow all fatal integrity checking errors to be +- -- # converted to non-fatal warnings +- -- +- -- | "Assertion-OnlineApproval:" url +- -- # require an online approval by receiving an "OK" from +- -- # specified remote service +- -- +- -- | "Assertion-OnlineReporting:" url +- -- # perform an asynchronous online reporting to +- -- # specified remote service +- -- +- -- | "Assertion-Prefix:" path +- -- # require %{l_prefix} to match specified path +- -- +- -- | "Assertion-User:" user +- -- # require %{l_musr} to match specified username +- -- +- -- | "Assertion-Group:" group +- -- # require %{l_mgrp} to match specified groupname +- -- +- -- | "Assertion-Domain:" domain +- -- # require domain of host to match specified domain name +- -- +- -- | "Assertion-LifeTime:" iso-date.":".iso-date +- -- # require current real-time to be within specified +- -- # begin and end date +- -- +- -- | "Assertion-GrantTime:" iso-date.":".iso-date +- -- # require current OpenPKG Framework %{RELEASE} +- -- # (release time) to be within specified begin and end +- -- # date +- -- +- -- | "Assertion-InstanceAge:" duration +- -- # require current OpenPKG Framework %{ORIGINTIME} +- -- # (first install time) to be within specified begin +- -- # and end date +- -- +- -- | "Assertion-FromSourceOnTarget:" yes-no +- -- # require either (if "yes") that all package +- -- # %{BUILDHOST} are equal the host name or (if "no") +- -- # that all package %{BUILDHOST} are not equal the host +- -- # name +- -- +- -- | "Assertion-PackageNames:" +- -- ("!"?.mode-regex.":"."!"?.package-regex)+ +- -- # require all package %{NAME} to (not) match the +- -- # specified regex while the current RPM run-time mode +- -- # has to (not) match the specified regex. RPM run-time +- -- # modes are: query, verify, checksig, resign, install, +- -- # erase, build rebuild, recompile, tarbuild, initdb, +- -- # rebuilddb and verifydb. +- -- +- -- | "Assertion-PackageReleaseAge:" +- -- percent.":".duration.":".dist-regex ((package-name|"*").":".release)+ +- -- # require that for at least the specified amount (in +- -- # percent) of packages, which %{DISTRIBUTION} matches +- -- # the specified regex, the %{RELEASE} is at least as +- -- # old as the specified release or at least not older +- -- # than the specified duration. +- -- +- -- | "Assertion-Expression:" expression +- -- # evaluates the Lua boolean expression after expanding +- -- # RPM macros %{VARNAME} and expanding the construct +- -- # "" ~~ // into the corresponding PCRE +- -- # based regular expression match. +- -- +- -- version ::= /^\d+\.\d+\.\d+$/ +- -- yes-no ::= /^yes|no$/ +- -- url ::= /^https?:\/\/.+$/ +- -- path ::= /^\/.+$/ +- -- user ::= /^[a-z][a-zA-Z0-9_]*$/ +- -- group ::= /^[a-z][a-zA-Z0-9_]*$/ +- -- domain ::= /^(?:[^.]+\.)+[^.]+$/ +- -- mode-regex ::= /^.+$/ +- -- package-regex ::= /^.+$/ +- -- package-name ::= /^[a-z][a-zA-Z0-9-]*$/ +- -- percent ::= /^\d+%$/ +- -- duration ::= /^\d+[smhdw]?$/ +- -- release ::= /^\d{8}$/ +- -- iso-date ::= /^\d{4}-\d{2}-\d{2}$/ +- -- expression ::= /^.+$/ + +- -- integrity processor version +integrity.version = "1.0.0" + +- -- integrity processor validation callback function +function integrity.validate(ctx, cfg) + integrity.util.debug(1, "OpenPKG run-time license integrity validation") + integrity.util.debug(4, function (ctx) return "dump: ctx = " .. util.dump(ctx) end, ctx) + integrity.util.debug(4, function (cfg) return "dump: cfg = " .. util.dump(cfg) end, cfg) + + -- process "Assertion-OnlineApproval" constraint + if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then + -- support explicitly requested license exception + cfg["Assertion-OnlineApproval"] = "http://openpkg.com/go/framework-license-exception" + end + if cfg["Assertion-OnlineApproval"] ~= nil then + integrity.util.debug(2, "checking: Assertion-OnlineApproval: \"%s\"", cfg["Assertion-OnlineApproval"]) + local uuids = integrity.util.uuids() + if uuids["UUID_REGISTRY"] == "" then + uuids["UUID_REGISTRY"] = "unknown" + end + if uuids["UUID_INSTANCE"] == "" then + uuids["UUID_INSTANCE"] = "unknown" + end + if uuids["UUID_PLATFORM"] == "" then + uuids["UUID_PLATFORM"] = "unknown" + end + local request = cfg["Assertion-OnlineApproval"] + request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"] + request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"] + request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"] + integrity.util.debug(3, "info: remote request \"%s\"", request) + local response = rpm.expand("%(%{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "')") + integrity.util.debug(3, "info: remote response \"%s\"", response) + if util.rmatch(response, "(?s)^\\s*OK\\s*$") then + -- approved + if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then + -- support explicitly requested license exception + cfg["Assertion-ErrorToWarning"] = "yes" + end + else + -- rejected + cfg["Assertion-ErrorToWarning"] = "no" + return integrity.util.error(ctx, cfg, + "license requires online approval but we failed to get " .. + "an \"OK\" response from the online service") + end + end + + -- process "Assertion-MinProcVersion" constraint + integrity.util.debug(2, "checking: Assertion-MinProcVersion: \"%s\"", cfg["Assertion-MinProcVersion"]) + if cfg["Assertion-MinProcVersion"] == nil then + return integrity.util.error(ctx, cfg, + "license configuration is missing required \"Assertion-MinProcVersion\" parameter") + end + integrity.util.debug(3, "require: %s <= %s", cfg["Assertion-MinProcVersion"], integrity.version) + if rpm.vercmp(cfg["Assertion-MinProcVersion"], integrity.version) > 0 then + return integrity.util.error(ctx, cfg, + "license configuration requires a license processor of " .. + "at least version \"" .. cfg["Assertion-MinProcVersion"] .. "\"") + end + + -- process "Assertion-OnlineReporting" constraint + if cfg["Assertion-OnlineReporting"] ~= nil then + integrity.util.debug(2, "checking: Assertion-OnlineReporting: \"%s\"", cfg["Assertion-OnlineReporting"]) + local uuids = integrity.util.uuids() + if uuids["UUID_REGISTRY"] == "" then + uuids["UUID_REGISTRY"] = "unknown" + end + if uuids["UUID_INSTANCE"] == "" then + uuids["UUID_INSTANCE"] = "unknown" + end + if uuids["UUID_PLATFORM"] == "" then + uuids["UUID_PLATFORM"] = "unknown" + end + local request = cfg["Assertion-OnlineReporting"] + request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"] + request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"] + request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"] + integrity.util.debug(3, "info: remote request \"%s\"", request) + rpm.expand("%(nohup %{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "' >/dev/null 2>&1 &)") + integrity.util.debug(3, "response: (ignored, because asynchronous operation)") + end + + -- process "Assertion-Prefix" constraint + if cfg["Assertion-Prefix"] ~= nil then + integrity.util.debug(2, "checking: Assertion-Prefix: \"%s\"", cfg["Assertion-Prefix"]) + local prefix = rpm.expand("%{l_prefix}") + integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Prefix"], prefix) + if cfg["Assertion-Prefix"] ~= prefix then + return integrity.util.error(ctx, cfg, + "instance prefix \"" .. prefix .. "\" " .. + "does not match value \"" .. cfg["Assertion-Prefix"] .. "\" of " .. + "license configuration parameter \"Assertion-Prefix\"") + end + end + + -- process "Assertion-User" constraint + if cfg["Assertion-User"] ~= nil then + integrity.util.debug(2, "checking: Assertion-User: \"%s\"", cfg["Assertion-User"]) + local user = rpm.expand("%{l_musr}") + integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-User"], user) + if cfg["Assertion-User"] ~= user then + return integrity.util.error(ctx, cfg, + "instance management user \"" .. user .. "\" " .. + "does not match value \"" .. cfg["Assertion-User"] .. "\" of " .. + "license configuration parameter \"Assertion-User\"") + end + end + + -- process "Assertion-Group" constraint + if cfg["Assertion-Group"] ~= nil then + integrity.util.debug(2, "checking: Assertion-Group: \"%s\"", cfg["Assertion-Group"]) + local group = rpm.expand("%{l_mgrp}") + integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Group"], group) + if cfg["Assertion-Group"] ~= group then + return integrity.util.error(ctx, cfg, + "instance management group \"" .. group .. "\" " .. + "does not match value \"" .. cfg["Assertion-Group"] .. "\" of " .. + "license configuration parameter \"Assertion-Group\"") + end + end + + -- process "Assertion-Domain" constraint + if cfg["Assertion-Domain"] ~= nil then + integrity.util.debug(2, "checking: Assertion-Domain: \"%s\"", cfg["Assertion-Domain"]) + local domain = rpm.expand("%(%{l_shtool} echo -n -e '%d')") + integrity.util.debug(3, "require: \"%s\" ~~ /(?s)^.*%s$/", domain, cfg["Assertion-Domain"]) + local s, _, m = util.rmatch(domain, "(?s)^.*" .. cfg["Assertion-Domain"] .. "$") + if s == nil then + return integrity.util.error(ctx, cfg, + "host domain \"" .. domain .. "\" " .. + "does not end in pattern \"" .. cfg["Assertion-Domain"] .. "\") " .. + "of license configuration parameter \"Assertion-Domain\"") + end + end + + -- process "Assertion-LifeTime" constraint + if cfg["Assertion-LifeTime"] ~= nil then + integrity.util.debug(2, "checking: Assertion-LifeTime: \"%s\"", cfg["Assertion-LifeTime"]) + + -- determine lifetime begin and end + local lifetime = cfg["Assertion-LifeTime"] + local s, _, m = util.rmatch(lifetime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$") + if s == nil then + return integrity.util.error(ctx, cfg, + "failed to extract time information from " .. + "license configuration parameter \"Assertion-LifeTime\"") + end + local lifetime_begin = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = 0, + min = 0, + sec = 0 + }) + local lifetime_end = os.time({ + year = tonumber(m[4]), + month = tonumber(m[5]), + day = tonumber(m[6]), + hour = 23, + min = 59, + sec = 59 + }) + + -- check whether current run-time is within lifetime + local t_now = os.time() + integrity.util.debug(3, "require: %d <= %d <= %d", lifetime_begin, t_now, lifetime_end) + if not (lifetime_begin <= t_now and t_now <= lifetime_end) then + return integrity.util.error(ctx, cfg, + "current time \"" .. os.date("!%Y-%m-%d %H:%M:%S UTC", t_now) .. "\" " .. + "is not within the timerange \"" .. cfg["Assertion-LifeTime"] .. "\" " .. + "of license configuration parameter \"Assertion-LifeTime\"") + end + end + + -- process "Assertion-GrantTime" constraint + if cfg["Assertion-GrantTime"] ~= nil then + integrity.util.debug(2, "checking: Assertion-GrantTime: \"%s\"", cfg["Assertion-GrantTime"]) + + -- determine granttime begin and end + local granttime = cfg["Assertion-GrantTime"] + local s, _, m = util.rmatch(granttime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$") + if s == nil then + return integrity.util.error(ctx, cfg, + "failed to extract time information from " .. + "license configuration parameter \"Assertion-GrantTime\"") + end + local granttime_begin = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = 0, + min = 0, + sec = 0 + }) + local granttime_end = os.time({ + year = tonumber(m[4]), + month = tonumber(m[5]), + day = tonumber(m[6]), + hour = 23, + min = 59, + sec = 59 + }) + + -- determine OpenPKG Framework release time + -- (allow openpkg.spec:%pre to override with a higher value for pre-checking) + local t_release = 0 + local result = {} + for _, line in ipairs(rpm.query("Q:%{RELEASE}", false, "openpkg")) do + local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$") + if s ~= nil then + table.insert(result, m[1]) + end + end + if result[1] ~= nil then + local s, _, m = util.rmatch(result[1], "^(?s)(\\d{4})(\\d{2})(\\d{2})$") + if s ~= nil then + t_release = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = 0, + min = 0, + sec = 0 + }) + end + end + if t_release == 0 then + return integrity.util.error(ctx, cfg, + "failed to determine OpenPKG Framework release time") + end + local override = os.getenv("OPENPKG_FRAMEWORK_RELEASE") + if override ~= nil then + local s, _, m = util.rmatch(override, "^(?s)(\\d{4})(\\d{2})(\\d{2})$") + if s ~= nil then + local t_override = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = 0, + min = 0, + sec = 0 + }) + if t_release < t_override then + t_release = t_override + end + end + end + + -- check whether current OpenPKG Framework release time is within granttime + integrity.util.debug(3, "require: %d <= %d <= %d", granttime_begin, t_release, granttime_end) + if not (granttime_begin <= t_release and t_release <= granttime_end) then + return integrity.util.error(ctx, cfg, + "current OpenPKG Framework release time \"" .. os.date("%Y-%m-%d", t_release) .. "\" " .. + "is not within the timerange \"" .. cfg["Assertion-GrantTime"] .. "\" " .. + "of license configuration parameter \"Assertion-GrantTime\"") + end + end + + -- process "Assertion-InstanceAge" constraint + if cfg["Assertion-InstanceAge"] ~= nil then + integrity.util.debug(2, "checking: Assertion-InstanceAge: \"%s\"", cfg["Assertion-InstanceAge"]) + + -- determine maximum instance age in seconds + local t_diff_max = cfg["Assertion-InstanceAge"] + t_diff_max = 0 + util.rsubst(t_diff_max, "^(\\d+)([smhdw])$", function (t, unit) + if unit == "s" then t = t * 1 + elseif unit == "m" then t = t * 60 + elseif unit == "h" then t = t * 60 * 60 + elseif unit == "d" then t = t * 60 * 60 * 24 + elseif unit == "w" then t = t * 60 * 60 * 24 * 7 + end + return t + end) + + -- approach 1: determine install time via timestamp of UUID_REGISTRY + local uuids = integrity.util.uuids() + if uuids["UUID_REGISTRY"] == "" then + return integrity.util.error(ctx, cfg, + "failed to load UUID_REGISTRY") + end + txt = uuid.describe(uuids["UUID_REGISTRY"]) + if txt == nil then + return integrity.util.error(ctx, cfg, + "failed to parse extracted UUID_REGISTRY string \"" .. uuids["UUID_REGISTRY"] .. "\" as an UUID") + end + local s, _, m = util.rmatch(txt, "(?s)^.*time:\\s+(\\d{4})-(\\d{2})-(\\d{2})\\s+(\\d{2}):(\\d{2}):(\\d{2}).*$") + if s == nil then + return integrity.util.error(ctx, cfg, + "failed to extract timestamp from UUID_REGISTRY \"" .. uuids["UUID_REGISTRY"] .. "\"") + end + local t_install = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = tonumber(m[4]), + min = tonumber(m[5]), + sec = tonumber(m[6]) + }) + + -- approach 2: determine install time via first install time of "openpkg" package + local result = {} + for _, line in ipairs(rpm.query( + "Q:%|ORIGINTIME?{" .. + "%{ORIGINTIME}" .. -- regular case: RPM 5 installed/updated with RPM 5 + "}:{" .. + "%|INSTALLTIME?{" .. + "%{INSTALLTIME}" .. -- special case: RPM 5 installed initially with RPM 4 + "}:{" .. + "}|" .. + "}|", false, "openpkg" + )) do + local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$") + if s ~= nil then + table.insert(result, m[1]) + end + end + if result[1] ~= nil then + local n = tonumber(result[1]) + if n > 0 then + t_install = n + end + end + + -- check time difference + local t_now = os.time() + local t_diff = os.difftime(t_now, t_install) + integrity.util.debug(3, "calc: %d - %d = %d", t_now, t_install, t_diff) + if t_diff < 0 then + return integrity.util.error(ctx, cfg, + "current system time \"" .. t_now .. "\" is lower than " .. + "instance installation time \"" .. t_install .. "\"") + end + integrity.util.debug(3, "require: %d <= %d", t_diff, t_diff_max) + if t_diff > t_diff_max then + return integrity.util.error(ctx, cfg, + "instance age \"" .. t_diff .. "\" " .. + "is greater than value \"" .. t_diff_max .. "\" (\"" .. cfg["Assertion-InstanceAge"] .. "\") " .. + "of license configuration parameter \"Assertion-InstanceAge\"") + end + end + + -- process "Assertion-FromSourceOnTarget" constraint + if cfg["Assertion-FromSourceOnTarget"] ~= nil then + integrity.util.debug(2, "checking: Assertion-FromSourceOnTarget: \"%s\"", cfg["Assertion-FromSourceOnTarget"]) + local hostname = rpm.hostname() + for _, line in ipairs(rpm.query("Q:%{NAME}:%{BUILDHOST}", true, "*")) do + local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(.+)$") + if s ~= nil then + local name = m[1] + local buildhost = m[2] + integrity.util.debug(4, "info: name \"%s\", buildhost \"%s\"", name, buildhost) + if not util.rmatch(name, "(?s)^gpg-.+$") and buildhost ~= "localhost" then + if cfg["Assertion-FromSourceOnTarget"] == "yes" and buildhost ~= hostname then + return integrity.util.error(ctx, cfg, + "license-required \"build from source on target system only\" situation not met because " .. + "package build host \"" .. buildhost .. "\" is not(!) equal to the package install host \"" .. hostname .. "\".") + end + if cfg["Assertion-FromSourceOnTarget"] == "no" and buildhost == hostname then + return integrity.util.error(ctx, cfg, + "license-required \"build binaries on separate build-host only\" situation not met because " .. + "package build host \"" .. buildhost .. "\" is equal to the package install host \"" .. hostname .. "\".") + end + end + end + end + end + + -- process "Assertion-PackageNames" constraints + if cfg["Assertion-PackageNames"] ~= nil then + integrity.util.debug(2, "checking: Assertion-PackageNames: \"%s\"", cfg["Assertion-PackageNames"]) + + -- query RPMDB for names of all installed packages + local packages = {} + for _, line in ipairs(rpm.query("Q:%{NAME}", true, "*")) do + local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$") + if s ~= nil then + table.insert(packages, m[1]) + end + end + + -- iterate over all constraints + for _, constraint in + ipairs( + util.rsplit( + util.rsubst( + cfg["Assertion-PackageNames"], + "(?s)^\\s*(.+?)\\s*$", "%1" + ), + "(?s)\\s+" + ) + ) do + -- parse constraint + local s, _, m = util.rmatch(constraint, "(?s)^(!?)([^:]+):(!?)(.+)$") + if s == nil then + return integrity.util.error(ctx, cfg, + "invalid syntax in license configuration \"Assertion-PackageNames\" " .. + "parameter: \"" .. constraint .. "\"") + end + local mode_negate = m[1] ~= "" + local mode_regex = m[2] + local package_negate = m[3] ~= "" + local package_regex = m[4] + -- apply the mode filter + local mode_matches, _, _ = util.rmatch(ctx.rpm.mode, mode_regex); + if (not mode_negate and mode_matches ~= nil) + or ( mode_negate and mode_matches == nil) then + -- apply the package filter to names of all installed packages + for _, package in ipairs(packages) do + if package_negate then + integrity.util.debug(3, "require: \"%s\" !~ /%s/", package, package_regex) + else + integrity.util.debug(3, "require: \"%s\" ~~ /%s/", package, package_regex) + end + local package_matches, _, _ = util.rmatch(package, package_regex) + if not ( (not package_negate and package_matches ~= nil) + or ( package_negate and package_matches == nil)) then + -- indicate integrity validation error + return integrity.util.error(ctx, cfg, + "installed package \"" .. package .. "\" " .. + "under RPM run-time mode \"" .. ctx.rpm.mode .. "\" " .. + "not covered by pattern \"" .. package_regex .. "\" " .. + "of license configuration parameter \"Assertion-PackageNames\"") + end + end + end + end + end + + -- process "Assertion-PackageReleaseAge" + if cfg["Assertion-PackageReleaseAge"] ~= nil then + integrity.util.debug(2, "checking: Assertion-PackageReleaseAge: \"%s[...]\"", string.sub(cfg["Assertion-PackageReleaseAge"], 1, 20)) + + -- parse constraint + local constraint = cfg["Assertion-PackageReleaseAge"] + local s, _, m = util.rmatch(constraint, "(?s)^([^:]+)%:([^:]+):([^\\s]+)\\s+(.+)$") + if s == nil then + return integrity.util.error(ctx, cfg, + "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" parameter") + end + local percent = m[1] / 100 + local offset = m[2] + local distregex = m[3] + local spec = m[4] + + -- determine maximum release time difference (in seconds) + local t_diff_max = 0 + util.rsubst(offset, "^(\\d+)([smhdw])$", function (t, unit) + if unit == "s" then t = t * 1 + elseif unit == "m" then t = t * 60 + elseif unit == "h" then t = t * 60 * 60 + elseif unit == "d" then t = t * 60 * 60 * 24 + elseif unit == "w" then t = t * 60 * 60 * 24 * 7 + end + return t + end) + + -- iterate over all package specifications to build release map + local releases = {} + for _, constraint in + ipairs( + util.rsplit( + util.rsubst( + spec, + "(?s)^\\s*(.+?)\\s*$", "%1" + ), + "(?s)\\s+" + ) + ) do + + -- parse specification into package name and release constraint + local s, _, m = util.rmatch(constraint, "(?s)^([^:]+):(.+)$") + if s == nil then + return integrity.util.error(ctx, cfg, + "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" " .. + "parameter: \"" .. constraint .. "\"") + end + + -- store result into release map + releases[m[1]] = m[2] + end + + -- query RPMDB for releases of all installed packages and decide + -- whether the release time is inside or outside our constraint window + local release_window_inside = 0 + local release_window_outside = 0 + local release_window_foreign = 0 + local release_window_unknown = 0 + for _, line in ipairs(rpm.query("Q:%{NAME}:%{RELEASE}:%{DISTRIBUTION}", true, "*")) do + local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(\\d\\d\\d\\d)(\\d\\d)(\\d\\d):(.+)$") + if s ~= nil then + -- parse query results + local name = m[1] + local t_release = os.time({ + year = tonumber(m[2]), + month = tonumber(m[3]), + day = tonumber(m[4]), + hour = 23, + min = 59, + sec = 59 + }) + local dist = m[5] + + -- only check files of the constrained distribution(s) + if util.rmatch(dist, "(?s)" .. distregex) then + + -- determine minimum release constraint + local t_release_min = releases[name] + if t_release_min == nil then + t_release_min = releases["*"] + end + if t_release_min == nil then + t_release_min = os.time() + else + local s, _, m = util.rmatch(t_release_min, "^(?s)(\\d{4})(\\d{2})(\\d{2})$") + t_release_min = os.time({ + year = tonumber(m[1]), + month = tonumber(m[2]), + day = tonumber(m[3]), + hour = 0, + min = 0, + sec = 0 + }) + end + + -- check time difference of package release + local t_diff = os.difftime(t_release_min, t_release) + integrity.util.debug(4, "calc: %d - %d = %d", t_release_min, t_release, t_diff) + integrity.util.debug(4, "require: %d <= 0 or (%d > 0 and %d < %d)", t_diff, t_diff, t_diff, t_diff_max) + if t_diff <= 0 or (t_diff > 0 and t_diff < t_diff_max) then + release_window_inside = release_window_inside + 1 + else + release_window_outside = release_window_outside + 1 + end + else + release_window_foreign = release_window_foreign + 1 + end + else + release_window_unknown = release_window_unknown + 1 + end + end + integrity.util.debug(3, "info: inside %d, outside %d, foreign %d, unknown %d", + release_window_inside, release_window_outside, release_window_foreign, release_window_unknown) + + -- check validity of overall constraint + local percent_inside = + (release_window_inside / (release_window_inside + release_window_outside)) + integrity.util.debug(3, "require: %d >= %d", percent_inside, percent) + if percent_inside < percent then + return integrity.util.error(ctx, cfg, + "there are only " .. math.floor(percent_inside * 100) .. "% " .. + "packages inside the release date constraint " .. + "(expected a minimum of " .. math.floor(percent * 100) .. "%)") + end + end + + -- process "Assertion-Expression" constraint + if cfg["Assertion-Expression"] ~= nil then + integrity.util.debug(2, "checking: Assertion-Expression: \"%s\"", cfg["Assertion-Expression"]) + + -- expand special consytructs in expression + local expr = cfg["Assertion-Expression"] + expr = util.rsubst(expr, "(%\{[a-zA-Z_][a-zA-Z0-9_]+\})", function (str) + return rpm.expand(str) + end) + expr = util.rsubst(expr, "\"((?:\\\\.|[^\"])+)\"\\s*~~\\s*/((?:\\\\.|[^/])+)/", function (str, regex) + if util.rmatch(str, "(?s)" .. regex) ~= nil then + return "true" + else + return "false" + end + end) + + -- evaluate expression + integrity.util.debug(3, "evaluate: %s", expr) + result = assert(loadstring(expr))() + if type(result) ~= "boolean" then + result = false + end + if not result then + return integrity.util.error(ctx, cfg, + "expression \"" .. cfg["Assertion-Expression"] .. "\" " .. + "of license configuration parameter \"Assertion-Expression\"" .. + "evaluated to false") + end + end + + -- indicate license integrity validation success + return "OK" +end + +- -- integrity processor utilities namespace +integrity.util = {} + +- -- write debug information to stderr +function integrity.util.debug(level_this, msg, ...) + local level_min = os.getenv("OPENPKG_LICENSE_DEBUG") + if level_min ~= nil then + if type(level_min) == "string" then + level_min = tonumber(level_min) + end + if type(level_min) ~= "number" then + level_min = 0 + end + if level_this <= level_min then + local output + if type(msg) == "function" then + output = msg(...) + else + output = string.format(msg, ...) + end + local prefix = "" + local i = 1 + while (i < level_this) do + prefix = prefix .. " " + i = i + 1 + end + io.stderr:write("rpm: DEBUG: " .. prefix .. output .. "\n") + end + end +end + +- -- load OpenPKG instance UUIDs +function integrity.util.uuids() + local uuids = { + UUID_REGISTRY = "", + UUID_INSTANCE = "", + UUID_PLATFORM = "" + } + local filename = rpm.expand("%{l_prefix}/etc/openpkg/uuid") + local txt = rpm.slurp(filename) + if txt ~= nil then + for name, _ in pairs(uuids) do + local s, _, m = util.rmatch(txt, "(?s)^.*" .. name .. "=\"([^\"\"]+)\".*$") + if s ~= nil then + uuids[name] = m[1] + end + end + end + return uuids +end + +- -- report validation warning +function integrity.util.warning(ctx, cfg, warning) + -- return prominent warning message + return + "WARNING: OpenPKG run-time license check failed -- continue processing\n" .. + "+-----------------------------------------------------------------------------+\n" .. + "| Attention, the OpenPKG RPM run-time integrity checking facility encountered a\n" .. + "| non-fatal problem during license checking, but allows processing to continue.\n" .. + "| The particular warning reported by the OpenPKG license processor is:\n" .. + "|\n" .. + util.textwrap("| ", warning, 60, 70) .. + "|\n" .. + "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" .. + "| Go to http://openpkg.com/go/framework-license for more details, please.\n" .. + "+-----------------------------------------------------------------------------+" +end + +- -- report validation error +function integrity.util.error(ctx, cfg, error) + -- support conversion of errors into warnings + if cfg["Assertion-ErrorToWarning"] ~= nil then + if cfg["Assertion-ErrorToWarning"] == "yes" then + return integrity.util.warning(ctx, cfg, error) + end + end + + -- return prominent error message + return + "ERROR: OpenPKG run-time license check failed -- stopping processing\n" .. + "+-----------------------------------------------------------------------------+\n" .. + "| Sorry, the OpenPKG RPM run-time integrity checking facility encountered a\n" .. + "| fatal problem during license checking and stops processing immediately.\n" .. + "| The particular error reported by the OpenPKG license processor is:\n" .. + "|\n" .. + util.textwrap("| ", error, 60, 70) .. + "|\n" .. + "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" .. + "| Go to http://openpkg.com/go/framework-license for more details, please.\n" .. + "| Run \"openpkg man license\" for details about local license management.\n" .. + "+-----------------------------------------------------------------------------+" +end + +-----BEGIN PGP SIGNATURE----- +Comment: OpenPKG GmbH + +iEYEARECAAYFAk8BelcACgkQZwQuyWG3rjTL6QCeLTLVj4PTnd/E7mf+Sv4mgbZj +5J0AoMXrO4EimPSSCZSJ1TLW8f8GP+B5 +=AVpf +-----END PGP SIGNATURE-----