openpkg/license.lua

changeset 428
f880f219c566
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/openpkg/license.lua	Tue Jul 31 12:23:42 2012 +0200
     1.3 @@ -0,0 +1,808 @@
     1.4 +-----BEGIN PGP SIGNED MESSAGE-----
     1.5 +Hash: SHA1
     1.6 +
     1.7 +- --
     1.8 +- --  OpenPKG Framework License Processor
     1.9 +- --  Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/>
    1.10 +- --
    1.11 +- --  This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
    1.12 +- --  All rights reserved. Licenses which grant limited permission to use,
    1.13 +- --  copy, modify and distribute this software are available from the
    1.14 +- --  OpenPKG GmbH.
    1.15 +- --
    1.16 +- --  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
    1.17 +- --  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
    1.18 +- --  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    1.19 +- --  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
    1.20 +- --  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    1.21 +- --  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    1.22 +- --  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
    1.23 +- --  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    1.24 +- --  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    1.25 +- --  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    1.26 +- --  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    1.27 +- --  SUCH DAMAGE.
    1.28 +- --
    1.29 +
    1.30 +- --  This is the RPM run-time integrity processor of the OpenPKG
    1.31 +- --  Framework. It currently checks the OpenPKG Framework run-time
    1.32 +- --  license only. The following grammar specifies and documents all
    1.33 +- --  currently supported license parameters.
    1.34 +- --
    1.35 +- --  license ::= "Assertion-MinProcVersion:" version
    1.36 +- --              # require a minimum version of the license integrity processor
    1.37 +- --              
    1.38 +- --              | "Assertion-ErrorToWarning:" yes-no
    1.39 +- --              # allow all fatal integrity checking errors to be
    1.40 +- --              # converted to non-fatal warnings
    1.41 +- --              
    1.42 +- --              | "Assertion-OnlineApproval:" url
    1.43 +- --              # require an online approval by receiving an "OK" from
    1.44 +- --              # specified remote service
    1.45 +- --              
    1.46 +- --              | "Assertion-OnlineReporting:" url
    1.47 +- --              # perform an asynchronous online reporting to
    1.48 +- --              # specified remote service
    1.49 +- --              
    1.50 +- --              | "Assertion-Prefix:" path
    1.51 +- --              # require %{l_prefix} to match specified path
    1.52 +- --              
    1.53 +- --              | "Assertion-User:" user
    1.54 +- --              # require %{l_musr} to match specified username
    1.55 +- --              
    1.56 +- --              | "Assertion-Group:" group
    1.57 +- --              # require %{l_mgrp} to match specified groupname
    1.58 +- --              
    1.59 +- --              | "Assertion-Domain:" domain
    1.60 +- --              # require domain of host to match specified domain name
    1.61 +- --              
    1.62 +- --              | "Assertion-LifeTime:" iso-date.":".iso-date
    1.63 +- --              # require current real-time to be within specified
    1.64 +- --              # begin and end date
    1.65 +- --              
    1.66 +- --              | "Assertion-GrantTime:" iso-date.":".iso-date
    1.67 +- --              # require current OpenPKG Framework %{RELEASE}
    1.68 +- --              # (release time) to be within specified begin and end
    1.69 +- --              # date
    1.70 +- --              
    1.71 +- --              | "Assertion-InstanceAge:" duration
    1.72 +- --              # require current OpenPKG Framework %{ORIGINTIME}
    1.73 +- --              # (first install time) to be within specified begin
    1.74 +- --              # and end date
    1.75 +- --              
    1.76 +- --              | "Assertion-FromSourceOnTarget:" yes-no
    1.77 +- --              # require either (if "yes") that all package
    1.78 +- --              # %{BUILDHOST} are equal the host name or (if "no")
    1.79 +- --              # that all package %{BUILDHOST} are not equal the host
    1.80 +- --              # name
    1.81 +- --              
    1.82 +- --              | "Assertion-PackageNames:" 
    1.83 +- --                ("!"?.mode-regex.":"."!"?.package-regex)+
    1.84 +- --              # require all package %{NAME} to (not) match the
    1.85 +- --              # specified regex while the current RPM run-time mode
    1.86 +- --              # has to (not) match the specified regex. RPM run-time
    1.87 +- --              # modes are: query, verify, checksig, resign, install,
    1.88 +- --              # erase, build rebuild, recompile, tarbuild, initdb,
    1.89 +- --              # rebuilddb and verifydb.
    1.90 +- --              
    1.91 +- --              | "Assertion-PackageReleaseAge:"
    1.92 +- --                percent.":".duration.":".dist-regex ((package-name|"*").":".release)+
    1.93 +- --              # require that for at least the specified amount (in
    1.94 +- --              # percent) of packages, which %{DISTRIBUTION} matches
    1.95 +- --              # the specified regex, the %{RELEASE} is at least as
    1.96 +- --              # old as the specified release or at least not older
    1.97 +- --              # than the specified duration.
    1.98 +- --
    1.99 +- --              | "Assertion-Expression:" expression
   1.100 +- --              # evaluates the Lua boolean expression after expanding
   1.101 +- --              # RPM macros %{VARNAME} and expanding the construct
   1.102 +- --              # "<string>" ~~ /<regex>/ into the corresponding PCRE
   1.103 +- --              # based regular expression match.
   1.104 +- --  
   1.105 +- --  version       ::= /^\d+\.\d+\.\d+$/
   1.106 +- --  yes-no        ::= /^yes|no$/
   1.107 +- --  url           ::= /^https?:\/\/.+$/
   1.108 +- --  path          ::= /^\/.+$/
   1.109 +- --  user          ::= /^[a-z][a-zA-Z0-9_]*$/
   1.110 +- --  group         ::= /^[a-z][a-zA-Z0-9_]*$/
   1.111 +- --  domain        ::= /^(?:[^.]+\.)+[^.]+$/
   1.112 +- --  mode-regex    ::= /^.+$/
   1.113 +- --  package-regex ::= /^.+$/
   1.114 +- --  package-name  ::= /^[a-z][a-zA-Z0-9-]*$/
   1.115 +- --  percent       ::= /^\d+%$/
   1.116 +- --  duration      ::= /^\d+[smhdw]?$/
   1.117 +- --  release       ::= /^\d{8}$/
   1.118 +- --  iso-date      ::= /^\d{4}-\d{2}-\d{2}$/
   1.119 +- --  expression    ::= /^.+$/
   1.120 +
   1.121 +- --  integrity processor version
   1.122 +integrity.version = "1.0.0"
   1.123 +
   1.124 +- --  integrity processor validation callback function
   1.125 +function integrity.validate(ctx, cfg)
   1.126 +    integrity.util.debug(1, "OpenPKG run-time license integrity validation")
   1.127 +    integrity.util.debug(4, function (ctx) return "dump: ctx = " .. util.dump(ctx) end, ctx)
   1.128 +    integrity.util.debug(4, function (cfg) return "dump: cfg = " .. util.dump(cfg) end, cfg)
   1.129 +
   1.130 +    --  process "Assertion-OnlineApproval" constraint
   1.131 +    if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then
   1.132 +        --  support explicitly requested license exception
   1.133 +        cfg["Assertion-OnlineApproval"] = "http://openpkg.com/go/framework-license-exception"
   1.134 +    end
   1.135 +    if cfg["Assertion-OnlineApproval"] ~= nil then
   1.136 +        integrity.util.debug(2, "checking: Assertion-OnlineApproval: \"%s\"", cfg["Assertion-OnlineApproval"])
   1.137 +        local uuids = integrity.util.uuids()
   1.138 +        if  uuids["UUID_REGISTRY"] == "" then
   1.139 +            uuids["UUID_REGISTRY"] = "unknown"
   1.140 +        end
   1.141 +        if  uuids["UUID_INSTANCE"] == "" then
   1.142 +            uuids["UUID_INSTANCE"] = "unknown"
   1.143 +        end
   1.144 +        if  uuids["UUID_PLATFORM"] == "" then
   1.145 +            uuids["UUID_PLATFORM"] = "unknown"
   1.146 +        end
   1.147 +        local request = cfg["Assertion-OnlineApproval"]
   1.148 +        request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"]
   1.149 +        request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"]
   1.150 +        request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"]
   1.151 +        integrity.util.debug(3, "info: remote request \"%s\"", request)
   1.152 +        local response = rpm.expand("%(%{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "')")
   1.153 +        integrity.util.debug(3, "info: remote response \"%s\"", response)
   1.154 +        if util.rmatch(response, "(?s)^\\s*OK\\s*$") then
   1.155 +            --  approved
   1.156 +            if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then
   1.157 +                --  support explicitly requested license exception
   1.158 +                cfg["Assertion-ErrorToWarning"] = "yes"
   1.159 +            end
   1.160 +        else
   1.161 +            --  rejected
   1.162 +            cfg["Assertion-ErrorToWarning"] = "no"
   1.163 +            return integrity.util.error(ctx, cfg,
   1.164 +                "license requires online approval but we failed to get " ..
   1.165 +                "an \"OK\" response from the online service")
   1.166 +        end
   1.167 +    end
   1.168 +
   1.169 +    --  process "Assertion-MinProcVersion" constraint
   1.170 +    integrity.util.debug(2, "checking: Assertion-MinProcVersion: \"%s\"", cfg["Assertion-MinProcVersion"])
   1.171 +    if cfg["Assertion-MinProcVersion"] == nil then
   1.172 +        return integrity.util.error(ctx, cfg,
   1.173 +            "license configuration is missing required \"Assertion-MinProcVersion\" parameter")
   1.174 +    end
   1.175 +    integrity.util.debug(3, "require: %s <= %s", cfg["Assertion-MinProcVersion"], integrity.version)
   1.176 +    if rpm.vercmp(cfg["Assertion-MinProcVersion"], integrity.version) > 0 then
   1.177 +        return integrity.util.error(ctx, cfg,
   1.178 +            "license configuration requires a license processor of " ..
   1.179 +            "at least version \"" .. cfg["Assertion-MinProcVersion"] .. "\"")
   1.180 +    end
   1.181 +
   1.182 +    --  process "Assertion-OnlineReporting" constraint
   1.183 +    if cfg["Assertion-OnlineReporting"] ~= nil then
   1.184 +        integrity.util.debug(2, "checking: Assertion-OnlineReporting: \"%s\"", cfg["Assertion-OnlineReporting"])
   1.185 +        local uuids = integrity.util.uuids()
   1.186 +        if  uuids["UUID_REGISTRY"] == "" then
   1.187 +            uuids["UUID_REGISTRY"] = "unknown"
   1.188 +        end
   1.189 +        if  uuids["UUID_INSTANCE"] == "" then
   1.190 +            uuids["UUID_INSTANCE"] = "unknown"
   1.191 +        end
   1.192 +        if  uuids["UUID_PLATFORM"] == "" then
   1.193 +            uuids["UUID_PLATFORM"] = "unknown"
   1.194 +        end
   1.195 +        local request = cfg["Assertion-OnlineReporting"]
   1.196 +        request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"]
   1.197 +        request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"]
   1.198 +        request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"]
   1.199 +        integrity.util.debug(3, "info: remote request \"%s\"", request)
   1.200 +        rpm.expand("%(nohup %{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "' >/dev/null 2>&1 &)")
   1.201 +        integrity.util.debug(3, "response: (ignored, because asynchronous operation)")
   1.202 +    end
   1.203 +
   1.204 +    --  process "Assertion-Prefix" constraint
   1.205 +    if cfg["Assertion-Prefix"] ~= nil then
   1.206 +        integrity.util.debug(2, "checking: Assertion-Prefix: \"%s\"", cfg["Assertion-Prefix"])
   1.207 +        local prefix = rpm.expand("%{l_prefix}")
   1.208 +        integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Prefix"], prefix)
   1.209 +        if cfg["Assertion-Prefix"] ~= prefix then
   1.210 +            return integrity.util.error(ctx, cfg,
   1.211 +                "instance prefix \"" .. prefix .. "\" " ..
   1.212 +                "does not match value \"" .. cfg["Assertion-Prefix"] .. "\" of " ..
   1.213 +                "license configuration parameter \"Assertion-Prefix\"")
   1.214 +        end
   1.215 +    end
   1.216 +
   1.217 +    --  process "Assertion-User" constraint
   1.218 +    if cfg["Assertion-User"] ~= nil then
   1.219 +        integrity.util.debug(2, "checking: Assertion-User: \"%s\"", cfg["Assertion-User"])
   1.220 +        local user = rpm.expand("%{l_musr}")
   1.221 +        integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-User"], user)
   1.222 +        if cfg["Assertion-User"] ~= user then
   1.223 +            return integrity.util.error(ctx, cfg,
   1.224 +                "instance management user \"" .. user .. "\" " ..
   1.225 +                "does not match value \"" .. cfg["Assertion-User"] .. "\" of " ..
   1.226 +                "license configuration parameter \"Assertion-User\"")
   1.227 +        end
   1.228 +    end
   1.229 +
   1.230 +    --  process "Assertion-Group" constraint
   1.231 +    if cfg["Assertion-Group"] ~= nil then
   1.232 +        integrity.util.debug(2, "checking: Assertion-Group: \"%s\"", cfg["Assertion-Group"])
   1.233 +        local group = rpm.expand("%{l_mgrp}")
   1.234 +        integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Group"], group)
   1.235 +        if cfg["Assertion-Group"] ~= group then
   1.236 +            return integrity.util.error(ctx, cfg,
   1.237 +                "instance management group \"" .. group .. "\" " ..
   1.238 +                "does not match value \"" .. cfg["Assertion-Group"] .. "\" of " ..
   1.239 +                "license configuration parameter \"Assertion-Group\"")
   1.240 +        end
   1.241 +    end
   1.242 +
   1.243 +    --  process "Assertion-Domain" constraint
   1.244 +    if cfg["Assertion-Domain"] ~= nil then
   1.245 +        integrity.util.debug(2, "checking: Assertion-Domain: \"%s\"", cfg["Assertion-Domain"])
   1.246 +        local domain = rpm.expand("%(%{l_shtool} echo -n -e '%d')")
   1.247 +        integrity.util.debug(3, "require: \"%s\" ~~ /(?s)^.*%s$/", domain, cfg["Assertion-Domain"])
   1.248 +        local s, _, m = util.rmatch(domain, "(?s)^.*" .. cfg["Assertion-Domain"] .. "$")
   1.249 +        if s == nil then
   1.250 +            return integrity.util.error(ctx, cfg,
   1.251 +                "host domain \"" .. domain .. "\" " ..
   1.252 +                "does not end in pattern \"" .. cfg["Assertion-Domain"] .. "\") " ..
   1.253 +                "of license configuration parameter \"Assertion-Domain\"")
   1.254 +        end
   1.255 +    end
   1.256 +
   1.257 +    --  process "Assertion-LifeTime" constraint
   1.258 +    if cfg["Assertion-LifeTime"] ~= nil then
   1.259 +        integrity.util.debug(2, "checking: Assertion-LifeTime: \"%s\"", cfg["Assertion-LifeTime"])
   1.260 +
   1.261 +        --  determine lifetime begin and end
   1.262 +        local lifetime = cfg["Assertion-LifeTime"]
   1.263 +        local s, _, m = util.rmatch(lifetime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$")
   1.264 +        if s == nil then
   1.265 +            return integrity.util.error(ctx, cfg,
   1.266 +                "failed to extract time information from " ..
   1.267 +                "license configuration parameter \"Assertion-LifeTime\"")
   1.268 +        end
   1.269 +        local lifetime_begin = os.time({
   1.270 +            year  = tonumber(m[1]),
   1.271 +            month = tonumber(m[2]),
   1.272 +            day   = tonumber(m[3]),
   1.273 +            hour  = 0,
   1.274 +            min   = 0,
   1.275 +            sec   = 0
   1.276 +        })
   1.277 +        local lifetime_end = os.time({
   1.278 +            year  = tonumber(m[4]),
   1.279 +            month = tonumber(m[5]),
   1.280 +            day   = tonumber(m[6]),
   1.281 +            hour  = 23,
   1.282 +            min   = 59,
   1.283 +            sec   = 59
   1.284 +        })
   1.285 +
   1.286 +        --  check whether current run-time is within lifetime
   1.287 +        local t_now = os.time()
   1.288 +        integrity.util.debug(3, "require: %d <= %d <= %d", lifetime_begin, t_now, lifetime_end)
   1.289 +        if not (lifetime_begin <= t_now and t_now <= lifetime_end) then
   1.290 +            return integrity.util.error(ctx, cfg,
   1.291 +                "current time \"" .. os.date("!%Y-%m-%d %H:%M:%S UTC", t_now) .. "\" " ..
   1.292 +                "is not within the timerange \"" .. cfg["Assertion-LifeTime"] .. "\" " ..
   1.293 +                "of license configuration parameter \"Assertion-LifeTime\"")
   1.294 +        end
   1.295 +    end
   1.296 +
   1.297 +    --  process "Assertion-GrantTime" constraint
   1.298 +    if cfg["Assertion-GrantTime"] ~= nil then
   1.299 +        integrity.util.debug(2, "checking: Assertion-GrantTime: \"%s\"", cfg["Assertion-GrantTime"])
   1.300 +
   1.301 +        --  determine granttime begin and end
   1.302 +        local granttime = cfg["Assertion-GrantTime"]
   1.303 +        local s, _, m = util.rmatch(granttime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$")
   1.304 +        if s == nil then
   1.305 +            return integrity.util.error(ctx, cfg,
   1.306 +                "failed to extract time information from " ..
   1.307 +                "license configuration parameter \"Assertion-GrantTime\"")
   1.308 +        end
   1.309 +        local granttime_begin = os.time({
   1.310 +            year  = tonumber(m[1]),
   1.311 +            month = tonumber(m[2]),
   1.312 +            day   = tonumber(m[3]),
   1.313 +            hour  = 0,
   1.314 +            min   = 0,
   1.315 +            sec   = 0
   1.316 +        })
   1.317 +        local granttime_end = os.time({
   1.318 +            year  = tonumber(m[4]),
   1.319 +            month = tonumber(m[5]),
   1.320 +            day   = tonumber(m[6]),
   1.321 +            hour  = 23,
   1.322 +            min   = 59,
   1.323 +            sec   = 59
   1.324 +        })
   1.325 +
   1.326 +        --  determine OpenPKG Framework release time
   1.327 +        --  (allow openpkg.spec:%pre to override with a higher value for pre-checking)
   1.328 +        local t_release = 0
   1.329 +        local result = {}
   1.330 +        for _, line in ipairs(rpm.query("Q:%{RELEASE}", false, "openpkg")) do
   1.331 +            local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   1.332 +            if s ~= nil then
   1.333 +                table.insert(result, m[1])
   1.334 +            end
   1.335 +        end
   1.336 +        if result[1] ~= nil then
   1.337 +            local s, _, m = util.rmatch(result[1], "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   1.338 +            if s ~= nil then
   1.339 +                t_release = os.time({
   1.340 +                    year  = tonumber(m[1]),
   1.341 +                    month = tonumber(m[2]),
   1.342 +                    day   = tonumber(m[3]),
   1.343 +                    hour  = 0,
   1.344 +                    min   = 0,
   1.345 +                    sec   = 0
   1.346 +                })
   1.347 +            end
   1.348 +        end
   1.349 +        if t_release == 0 then
   1.350 +            return integrity.util.error(ctx, cfg,
   1.351 +                "failed to determine OpenPKG Framework release time")
   1.352 +        end
   1.353 +        local override = os.getenv("OPENPKG_FRAMEWORK_RELEASE")
   1.354 +        if override ~= nil then
   1.355 +            local s, _, m = util.rmatch(override, "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   1.356 +            if s ~= nil then
   1.357 +                local t_override = os.time({
   1.358 +                    year  = tonumber(m[1]),
   1.359 +                    month = tonumber(m[2]),
   1.360 +                    day   = tonumber(m[3]),
   1.361 +                    hour  = 0,
   1.362 +                    min   = 0,
   1.363 +                    sec   = 0
   1.364 +                })
   1.365 +                if t_release < t_override then
   1.366 +                    t_release = t_override
   1.367 +                end
   1.368 +            end
   1.369 +        end
   1.370 +
   1.371 +        --  check whether current OpenPKG Framework release time is within granttime
   1.372 +        integrity.util.debug(3, "require: %d <= %d <= %d", granttime_begin, t_release, granttime_end)
   1.373 +        if not (granttime_begin <= t_release and t_release <= granttime_end) then
   1.374 +            return integrity.util.error(ctx, cfg,
   1.375 +                "current OpenPKG Framework release time \"" .. os.date("%Y-%m-%d", t_release) .. "\" " ..
   1.376 +                "is not within the timerange \"" .. cfg["Assertion-GrantTime"] .. "\" " ..
   1.377 +                "of license configuration parameter \"Assertion-GrantTime\"")
   1.378 +        end
   1.379 +    end
   1.380 +
   1.381 +    --  process "Assertion-InstanceAge" constraint
   1.382 +    if cfg["Assertion-InstanceAge"] ~= nil then
   1.383 +        integrity.util.debug(2, "checking: Assertion-InstanceAge: \"%s\"", cfg["Assertion-InstanceAge"])
   1.384 +
   1.385 +        --  determine maximum instance age in seconds
   1.386 +        local t_diff_max = cfg["Assertion-InstanceAge"]
   1.387 +        t_diff_max = 0 + util.rsubst(t_diff_max, "^(\\d+)([smhdw])$", function (t, unit)
   1.388 +            if     unit == "s" then t = t * 1
   1.389 +            elseif unit == "m" then t = t * 60
   1.390 +            elseif unit == "h" then t = t * 60 * 60
   1.391 +            elseif unit == "d" then t = t * 60 * 60 * 24
   1.392 +            elseif unit == "w" then t = t * 60 * 60 * 24 * 7
   1.393 +            end
   1.394 +            return t
   1.395 +        end)
   1.396 +
   1.397 +        --  approach 1: determine install time via timestamp of UUID_REGISTRY
   1.398 +        local uuids = integrity.util.uuids()
   1.399 +        if uuids["UUID_REGISTRY"] == "" then
   1.400 +            return integrity.util.error(ctx, cfg,
   1.401 +                "failed to load UUID_REGISTRY")
   1.402 +        end
   1.403 +        txt = uuid.describe(uuids["UUID_REGISTRY"])
   1.404 +        if txt == nil then
   1.405 +            return integrity.util.error(ctx, cfg,
   1.406 +                "failed to parse extracted UUID_REGISTRY string \"" .. uuids["UUID_REGISTRY"] .. "\" as an UUID")
   1.407 +        end
   1.408 +        local s, _, m = util.rmatch(txt, "(?s)^.*time:\\s+(\\d{4})-(\\d{2})-(\\d{2})\\s+(\\d{2}):(\\d{2}):(\\d{2}).*$")
   1.409 +        if s == nil then
   1.410 +            return integrity.util.error(ctx, cfg,
   1.411 +                "failed to extract timestamp from UUID_REGISTRY \"" .. uuids["UUID_REGISTRY"] .. "\"")
   1.412 +        end
   1.413 +        local t_install = os.time({
   1.414 +            year  = tonumber(m[1]),
   1.415 +            month = tonumber(m[2]),
   1.416 +            day   = tonumber(m[3]),
   1.417 +            hour  = tonumber(m[4]),
   1.418 +            min   = tonumber(m[5]),
   1.419 +            sec   = tonumber(m[6])
   1.420 +        })
   1.421 +
   1.422 +        --  approach 2: determine install time via first install time of "openpkg" package
   1.423 +        local result = {}
   1.424 +        for _, line in ipairs(rpm.query(
   1.425 +            "Q:%|ORIGINTIME?{" ..
   1.426 +                "%{ORIGINTIME}" ..       -- regular case: RPM 5 installed/updated with RPM 5
   1.427 +            "}:{" ..
   1.428 +                "%|INSTALLTIME?{" ..
   1.429 +                    "%{INSTALLTIME}" ..  -- special case: RPM 5 installed initially with RPM 4
   1.430 +                "}:{" ..
   1.431 +                "}|" ..
   1.432 +            "}|", false, "openpkg"
   1.433 +        )) do
   1.434 +            local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   1.435 +            if s ~= nil then
   1.436 +                table.insert(result, m[1])
   1.437 +            end
   1.438 +        end
   1.439 +        if result[1] ~= nil then
   1.440 +            local n = tonumber(result[1])
   1.441 +            if n > 0 then
   1.442 +                t_install = n
   1.443 +            end
   1.444 +        end
   1.445 +
   1.446 +        --  check time difference
   1.447 +        local t_now = os.time()
   1.448 +        local t_diff = os.difftime(t_now, t_install)
   1.449 +        integrity.util.debug(3, "calc: %d - %d = %d", t_now, t_install, t_diff)
   1.450 +        if t_diff < 0 then
   1.451 +            return integrity.util.error(ctx, cfg,
   1.452 +                "current system time \"" .. t_now .. "\" is lower than " ..
   1.453 +                "instance installation time \"" .. t_install .. "\"")
   1.454 +        end
   1.455 +        integrity.util.debug(3, "require: %d <= %d", t_diff, t_diff_max)
   1.456 +        if t_diff > t_diff_max then
   1.457 +            return integrity.util.error(ctx, cfg,
   1.458 +                "instance age \"" .. t_diff .. "\" " ..
   1.459 +                "is greater than value \"" .. t_diff_max .. "\" (\"" .. cfg["Assertion-InstanceAge"] .. "\") " ..
   1.460 +                "of license configuration parameter \"Assertion-InstanceAge\"")
   1.461 +        end
   1.462 +    end
   1.463 +
   1.464 +    --  process "Assertion-FromSourceOnTarget" constraint
   1.465 +    if cfg["Assertion-FromSourceOnTarget"] ~= nil then
   1.466 +        integrity.util.debug(2, "checking: Assertion-FromSourceOnTarget: \"%s\"", cfg["Assertion-FromSourceOnTarget"])
   1.467 +        local hostname = rpm.hostname()
   1.468 +        for _, line in ipairs(rpm.query("Q:%{NAME}:%{BUILDHOST}", true, "*")) do
   1.469 +            local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(.+)$")
   1.470 +            if s ~= nil then
   1.471 +                local name = m[1]
   1.472 +                local buildhost = m[2]
   1.473 +                integrity.util.debug(4, "info: name \"%s\", buildhost \"%s\"", name, buildhost)
   1.474 +                if not util.rmatch(name, "(?s)^gpg-.+$") and buildhost ~= "localhost" then
   1.475 +                    if cfg["Assertion-FromSourceOnTarget"] == "yes" and buildhost ~= hostname then
   1.476 +                        return integrity.util.error(ctx, cfg,
   1.477 +                            "license-required \"build from source on target system only\" situation not met because " ..
   1.478 +                            "package build host \"" .. buildhost .. "\" is not(!) equal to the package install host \"" .. hostname .. "\".")
   1.479 +                    end
   1.480 +                    if cfg["Assertion-FromSourceOnTarget"] == "no" and buildhost == hostname then
   1.481 +                        return integrity.util.error(ctx, cfg,
   1.482 +                            "license-required \"build binaries on separate build-host only\" situation not met because " ..
   1.483 +                            "package build host \"" .. buildhost .. "\" is equal to the package install host \"" .. hostname .. "\".")
   1.484 +                    end
   1.485 +                end
   1.486 +            end
   1.487 +        end
   1.488 +    end
   1.489 +
   1.490 +    --  process "Assertion-PackageNames" constraints
   1.491 +    if cfg["Assertion-PackageNames"] ~= nil then
   1.492 +        integrity.util.debug(2, "checking: Assertion-PackageNames: \"%s\"", cfg["Assertion-PackageNames"])
   1.493 +
   1.494 +        --  query RPMDB for names of all installed packages
   1.495 +        local packages = {}
   1.496 +        for _, line in ipairs(rpm.query("Q:%{NAME}", true, "*")) do
   1.497 +            local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   1.498 +            if s ~= nil then
   1.499 +                table.insert(packages, m[1])
   1.500 +            end
   1.501 +        end
   1.502 +
   1.503 +        --  iterate over all constraints
   1.504 +        for _, constraint in
   1.505 +            ipairs(
   1.506 +                util.rsplit(
   1.507 +                    util.rsubst(
   1.508 +                        cfg["Assertion-PackageNames"],
   1.509 +                        "(?s)^\\s*(.+?)\\s*$", "%1"
   1.510 +                    ),
   1.511 +                    "(?s)\\s+"
   1.512 +                )
   1.513 +            ) do
   1.514 +            --  parse constraint
   1.515 +            local s, _, m = util.rmatch(constraint, "(?s)^(!?)([^:]+):(!?)(.+)$")
   1.516 +            if s == nil then
   1.517 +                return integrity.util.error(ctx, cfg,
   1.518 +                    "invalid syntax in license configuration \"Assertion-PackageNames\" " ..
   1.519 +                    "parameter: \"" ..  constraint .. "\"")
   1.520 +            end
   1.521 +            local mode_negate    = m[1] ~= ""
   1.522 +            local mode_regex     = m[2]
   1.523 +            local package_negate = m[3] ~= ""
   1.524 +            local package_regex  = m[4]
   1.525 +            --  apply the mode filter
   1.526 +            local mode_matches, _, _ = util.rmatch(ctx.rpm.mode, mode_regex);
   1.527 +            if     (not mode_negate and mode_matches ~= nil)
   1.528 +                or (    mode_negate and mode_matches == nil) then
   1.529 +                --  apply the package filter to names of all installed packages
   1.530 +                for _, package in ipairs(packages) do
   1.531 +                    if package_negate then
   1.532 +                        integrity.util.debug(3, "require: \"%s\" !~ /%s/", package, package_regex)
   1.533 +                    else
   1.534 +                        integrity.util.debug(3, "require: \"%s\" ~~ /%s/", package, package_regex)
   1.535 +                    end
   1.536 +                    local package_matches, _, _ = util.rmatch(package, package_regex)
   1.537 +                    if  not (   (not package_negate and package_matches ~= nil)
   1.538 +                             or (    package_negate and package_matches == nil)) then
   1.539 +                        --  indicate integrity validation error
   1.540 +                        return integrity.util.error(ctx, cfg,
   1.541 +                            "installed package \"" .. package .. "\" " ..
   1.542 +                            "under RPM run-time mode \"" .. ctx.rpm.mode .. "\" " ..
   1.543 +                            "not covered by pattern \"" .. package_regex .. "\" " ..
   1.544 +                            "of license configuration parameter \"Assertion-PackageNames\"")
   1.545 +                    end
   1.546 +                end
   1.547 +            end
   1.548 +        end
   1.549 +    end
   1.550 +
   1.551 +    --  process "Assertion-PackageReleaseAge"
   1.552 +    if cfg["Assertion-PackageReleaseAge"] ~= nil then
   1.553 +        integrity.util.debug(2, "checking: Assertion-PackageReleaseAge: \"%s[...]\"", string.sub(cfg["Assertion-PackageReleaseAge"], 1, 20))
   1.554 +
   1.555 +        --  parse constraint
   1.556 +        local constraint = cfg["Assertion-PackageReleaseAge"]
   1.557 +        local s, _, m = util.rmatch(constraint, "(?s)^([^:]+)%:([^:]+):([^\\s]+)\\s+(.+)$")
   1.558 +        if s == nil then
   1.559 +            return integrity.util.error(ctx, cfg,
   1.560 +                "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" parameter")
   1.561 +        end
   1.562 +        local percent   = m[1] / 100
   1.563 +        local offset    = m[2]
   1.564 +        local distregex = m[3]
   1.565 +        local spec      = m[4]
   1.566 +
   1.567 +        --  determine maximum release time difference (in seconds)
   1.568 +        local t_diff_max = 0 + util.rsubst(offset, "^(\\d+)([smhdw])$", function (t, unit)
   1.569 +            if     unit == "s" then t = t * 1
   1.570 +            elseif unit == "m" then t = t * 60
   1.571 +            elseif unit == "h" then t = t * 60 * 60
   1.572 +            elseif unit == "d" then t = t * 60 * 60 * 24
   1.573 +            elseif unit == "w" then t = t * 60 * 60 * 24 * 7
   1.574 +            end
   1.575 +            return t
   1.576 +        end)
   1.577 +
   1.578 +        --  iterate over all package specifications to build release map
   1.579 +        local releases = {}
   1.580 +        for _, constraint in
   1.581 +            ipairs(
   1.582 +                util.rsplit(
   1.583 +                    util.rsubst(
   1.584 +                        spec,
   1.585 +                        "(?s)^\\s*(.+?)\\s*$", "%1"
   1.586 +                    ),
   1.587 +                    "(?s)\\s+"
   1.588 +                )
   1.589 +            ) do
   1.590 +
   1.591 +            --  parse specification into package name and release constraint
   1.592 +            local s, _, m = util.rmatch(constraint, "(?s)^([^:]+):(.+)$")
   1.593 +            if s == nil then
   1.594 +                return integrity.util.error(ctx, cfg,
   1.595 +                    "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" " ..
   1.596 +                    "parameter: \"" ..  constraint .. "\"")
   1.597 +            end
   1.598 +
   1.599 +            -- store result into release map
   1.600 +            releases[m[1]] = m[2]
   1.601 +        end
   1.602 +
   1.603 +        --  query RPMDB for releases of all installed packages and decide
   1.604 +        --  whether the release time is inside or outside our constraint window
   1.605 +        local release_window_inside  = 0
   1.606 +        local release_window_outside = 0
   1.607 +        local release_window_foreign = 0
   1.608 +        local release_window_unknown = 0
   1.609 +        for _, line in ipairs(rpm.query("Q:%{NAME}:%{RELEASE}:%{DISTRIBUTION}", true, "*")) do
   1.610 +            local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(\\d\\d\\d\\d)(\\d\\d)(\\d\\d):(.+)$")
   1.611 +            if s ~= nil then
   1.612 +                --  parse query results
   1.613 +                local name = m[1]
   1.614 +                local t_release = os.time({
   1.615 +                    year  = tonumber(m[2]),
   1.616 +                    month = tonumber(m[3]),
   1.617 +                    day   = tonumber(m[4]),
   1.618 +                    hour  = 23,
   1.619 +                    min   = 59,
   1.620 +                    sec   = 59
   1.621 +                })
   1.622 +                local dist = m[5]
   1.623 +
   1.624 +                --  only check files of the constrained distribution(s)
   1.625 +                if util.rmatch(dist, "(?s)" .. distregex) then
   1.626 +
   1.627 +                    --  determine minimum release constraint
   1.628 +                    local t_release_min = releases[name]
   1.629 +                    if t_release_min == nil then
   1.630 +                        t_release_min = releases["*"]
   1.631 +                    end
   1.632 +                    if t_release_min == nil then
   1.633 +                        t_release_min = os.time()
   1.634 +                    else
   1.635 +                        local s, _, m = util.rmatch(t_release_min, "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   1.636 +                        t_release_min = os.time({
   1.637 +                            year  = tonumber(m[1]),
   1.638 +                            month = tonumber(m[2]),
   1.639 +                            day   = tonumber(m[3]),
   1.640 +                            hour  = 0,
   1.641 +                            min   = 0,
   1.642 +                            sec   = 0
   1.643 +                        })
   1.644 +                    end
   1.645 +                    
   1.646 +                    --  check time difference of package release
   1.647 +                    local t_diff = os.difftime(t_release_min, t_release)
   1.648 +                    integrity.util.debug(4, "calc: %d - %d = %d", t_release_min, t_release, t_diff)
   1.649 +                    integrity.util.debug(4, "require: %d <= 0 or (%d > 0 and %d < %d)", t_diff, t_diff, t_diff, t_diff_max)
   1.650 +                    if t_diff <= 0 or (t_diff > 0 and t_diff < t_diff_max) then
   1.651 +                        release_window_inside  = release_window_inside  + 1
   1.652 +                    else
   1.653 +                        release_window_outside = release_window_outside + 1
   1.654 +                    end
   1.655 +                else
   1.656 +                    release_window_foreign = release_window_foreign + 1
   1.657 +                end
   1.658 +            else
   1.659 +                release_window_unknown = release_window_unknown + 1
   1.660 +            end
   1.661 +        end
   1.662 +        integrity.util.debug(3, "info: inside %d, outside %d, foreign %d, unknown %d",
   1.663 +            release_window_inside, release_window_outside, release_window_foreign, release_window_unknown)
   1.664 +
   1.665 +        --  check validity of overall constraint
   1.666 +        local percent_inside =
   1.667 +            (release_window_inside / (release_window_inside + release_window_outside))
   1.668 +        integrity.util.debug(3, "require: %d >= %d", percent_inside, percent)
   1.669 +        if percent_inside < percent then
   1.670 +            return integrity.util.error(ctx, cfg,
   1.671 +                "there are only " .. math.floor(percent_inside * 100) .. "% " ..
   1.672 +                "packages inside the release date constraint " ..
   1.673 +                "(expected a minimum of " .. math.floor(percent * 100) .. "%)")
   1.674 +        end
   1.675 +    end
   1.676 +
   1.677 +    --  process "Assertion-Expression" constraint
   1.678 +    if cfg["Assertion-Expression"] ~= nil then
   1.679 +        integrity.util.debug(2, "checking: Assertion-Expression: \"%s\"", cfg["Assertion-Expression"])
   1.680 +
   1.681 +        --  expand special consytructs in expression
   1.682 +        local expr = cfg["Assertion-Expression"]
   1.683 +        expr = util.rsubst(expr, "(%\{[a-zA-Z_][a-zA-Z0-9_]+\})", function (str)
   1.684 +            return rpm.expand(str)
   1.685 +        end)
   1.686 +        expr = util.rsubst(expr, "\"((?:\\\\.|[^\"])+)\"\\s*~~\\s*/((?:\\\\.|[^/])+)/", function (str, regex)
   1.687 +            if util.rmatch(str, "(?s)" .. regex) ~= nil then
   1.688 +                return "true"
   1.689 +            else
   1.690 +                return "false"
   1.691 +            end
   1.692 +        end)
   1.693 +
   1.694 +        --  evaluate expression
   1.695 +        integrity.util.debug(3, "evaluate: %s", expr)
   1.696 +        result = assert(loadstring(expr))()
   1.697 +        if type(result) ~= "boolean" then
   1.698 +            result = false
   1.699 +        end
   1.700 +        if not result then
   1.701 +            return integrity.util.error(ctx, cfg,
   1.702 +                "expression \"" .. cfg["Assertion-Expression"] .. "\" " ..
   1.703 +                "of license configuration parameter \"Assertion-Expression\"" ..
   1.704 +                "evaluated to false")
   1.705 +        end
   1.706 +    end
   1.707 +
   1.708 +    --  indicate license integrity validation success
   1.709 +    return "OK"
   1.710 +end
   1.711 +
   1.712 +- --  integrity processor utilities namespace
   1.713 +integrity.util = {}
   1.714 +
   1.715 +- --  write debug information to stderr
   1.716 +function integrity.util.debug(level_this, msg, ...)
   1.717 +    local level_min = os.getenv("OPENPKG_LICENSE_DEBUG")
   1.718 +    if level_min ~= nil then
   1.719 +        if type(level_min) == "string" then
   1.720 +            level_min = tonumber(level_min)
   1.721 +        end
   1.722 +        if type(level_min) ~= "number" then
   1.723 +            level_min = 0
   1.724 +        end
   1.725 +        if level_this <= level_min then
   1.726 +            local output
   1.727 +            if type(msg) == "function" then
   1.728 +                output = msg(...)
   1.729 +            else
   1.730 +                output = string.format(msg, ...)
   1.731 +            end
   1.732 +            local prefix = ""
   1.733 +            local i = 1
   1.734 +            while (i < level_this) do
   1.735 +                prefix = prefix .. "    "
   1.736 +                i = i + 1
   1.737 +            end
   1.738 +            io.stderr:write("rpm: DEBUG: " .. prefix .. output .. "\n")
   1.739 +        end
   1.740 +    end
   1.741 +end
   1.742 +
   1.743 +- --  load OpenPKG instance UUIDs
   1.744 +function integrity.util.uuids()
   1.745 +    local uuids = {
   1.746 +        UUID_REGISTRY = "",
   1.747 +        UUID_INSTANCE = "",
   1.748 +        UUID_PLATFORM = ""
   1.749 +    }
   1.750 +    local filename = rpm.expand("%{l_prefix}/etc/openpkg/uuid")
   1.751 +    local txt = rpm.slurp(filename)
   1.752 +    if txt ~= nil then
   1.753 +        for name, _ in pairs(uuids) do
   1.754 +            local s, _, m = util.rmatch(txt, "(?s)^.*" .. name .. "=\"([^\"\"]+)\".*$")
   1.755 +            if s ~= nil then
   1.756 +                uuids[name] = m[1]
   1.757 +            end
   1.758 +        end
   1.759 +    end
   1.760 +    return uuids
   1.761 +end
   1.762 +
   1.763 +- --  report validation warning
   1.764 +function integrity.util.warning(ctx, cfg, warning)
   1.765 +    --  return prominent warning message
   1.766 +    return
   1.767 +        "WARNING: OpenPKG run-time license check failed -- continue processing\n" ..
   1.768 +        "+-----------------------------------------------------------------------------+\n" ..
   1.769 +        "| Attention, the OpenPKG RPM run-time integrity checking facility encountered a\n" ..
   1.770 +        "| non-fatal problem during license checking, but allows processing to continue.\n" ..
   1.771 +        "| The particular warning reported by the OpenPKG license processor is:\n" ..
   1.772 +        "|\n" ..
   1.773 +        util.textwrap("|   ", warning, 60, 70) ..
   1.774 +        "|\n" ..
   1.775 +        "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" ..
   1.776 +        "| Go to http://openpkg.com/go/framework-license for more details, please.\n" ..
   1.777 +        "+-----------------------------------------------------------------------------+"
   1.778 +end
   1.779 +
   1.780 +- --  report validation error
   1.781 +function integrity.util.error(ctx, cfg, error)
   1.782 +    --  support conversion of errors into warnings
   1.783 +    if cfg["Assertion-ErrorToWarning"] ~= nil then
   1.784 +        if cfg["Assertion-ErrorToWarning"] == "yes" then
   1.785 +            return integrity.util.warning(ctx, cfg, error)
   1.786 +        end
   1.787 +    end
   1.788 +
   1.789 +    --  return prominent error message
   1.790 +    return
   1.791 +        "ERROR: OpenPKG run-time license check failed -- stopping processing\n" ..
   1.792 +        "+-----------------------------------------------------------------------------+\n" ..
   1.793 +        "| Sorry, the OpenPKG RPM run-time integrity checking facility encountered a\n" ..
   1.794 +        "| fatal problem during license checking and stops processing immediately.\n" ..
   1.795 +        "| The particular error reported by the OpenPKG license processor is:\n" ..
   1.796 +        "|\n" ..
   1.797 +        util.textwrap("|   ", error, 60, 70) ..
   1.798 +        "|\n" ..
   1.799 +        "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" ..
   1.800 +        "| Go to http://openpkg.com/go/framework-license for more details, please.\n" ..
   1.801 +        "| Run \"openpkg man license\" for details about local license management.\n" ..
   1.802 +        "+-----------------------------------------------------------------------------+"
   1.803 +end
   1.804 +
   1.805 +-----BEGIN PGP SIGNATURE-----
   1.806 +Comment: OpenPKG GmbH <openpkg@openpkg.com>
   1.807 +
   1.808 +iEYEARECAAYFAk8BelcACgkQZwQuyWG3rjTL6QCeLTLVj4PTnd/E7mf+Sv4mgbZj
   1.809 +5J0AoMXrO4EimPSSCZSJ1TLW8f8GP+B5
   1.810 +=AVpf
   1.811 +-----END PGP SIGNATURE-----

mercurial