openpkg/license.lua

Mon, 28 Jan 2013 17:37:18 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Mon, 28 Jan 2013 17:37:18 +0100
changeset 758
a2c6460cfb16
permissions
-rw-r--r--

Correct socket error reporting improvement with IPv6 portable code,
after helpful recommendation by Saúl Ibarra Corretgé on OSips devlist.

     1 -----BEGIN PGP SIGNED MESSAGE-----
     2 Hash: SHA1
     4 - --
     5 - --  OpenPKG Framework License Processor
     6 - --  Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/>
     7 - --
     8 - --  This software is property of the OpenPKG GmbH, DE MUC HRB 160208.
     9 - --  All rights reserved. Licenses which grant limited permission to use,
    10 - --  copy, modify and distribute this software are available from the
    11 - --  OpenPKG GmbH.
    12 - --
    13 - --  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
    14 - --  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
    15 - --  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    16 - --  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
    17 - --  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    18 - --  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    19 - --  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
    20 - --  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    21 - --  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    22 - --  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    23 - --  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    24 - --  SUCH DAMAGE.
    25 - --
    27 - --  This is the RPM run-time integrity processor of the OpenPKG
    28 - --  Framework. It currently checks the OpenPKG Framework run-time
    29 - --  license only. The following grammar specifies and documents all
    30 - --  currently supported license parameters.
    31 - --
    32 - --  license ::= "Assertion-MinProcVersion:" version
    33 - --              # require a minimum version of the license integrity processor
    34 - --              
    35 - --              | "Assertion-ErrorToWarning:" yes-no
    36 - --              # allow all fatal integrity checking errors to be
    37 - --              # converted to non-fatal warnings
    38 - --              
    39 - --              | "Assertion-OnlineApproval:" url
    40 - --              # require an online approval by receiving an "OK" from
    41 - --              # specified remote service
    42 - --              
    43 - --              | "Assertion-OnlineReporting:" url
    44 - --              # perform an asynchronous online reporting to
    45 - --              # specified remote service
    46 - --              
    47 - --              | "Assertion-Prefix:" path
    48 - --              # require %{l_prefix} to match specified path
    49 - --              
    50 - --              | "Assertion-User:" user
    51 - --              # require %{l_musr} to match specified username
    52 - --              
    53 - --              | "Assertion-Group:" group
    54 - --              # require %{l_mgrp} to match specified groupname
    55 - --              
    56 - --              | "Assertion-Domain:" domain
    57 - --              # require domain of host to match specified domain name
    58 - --              
    59 - --              | "Assertion-LifeTime:" iso-date.":".iso-date
    60 - --              # require current real-time to be within specified
    61 - --              # begin and end date
    62 - --              
    63 - --              | "Assertion-GrantTime:" iso-date.":".iso-date
    64 - --              # require current OpenPKG Framework %{RELEASE}
    65 - --              # (release time) to be within specified begin and end
    66 - --              # date
    67 - --              
    68 - --              | "Assertion-InstanceAge:" duration
    69 - --              # require current OpenPKG Framework %{ORIGINTIME}
    70 - --              # (first install time) to be within specified begin
    71 - --              # and end date
    72 - --              
    73 - --              | "Assertion-FromSourceOnTarget:" yes-no
    74 - --              # require either (if "yes") that all package
    75 - --              # %{BUILDHOST} are equal the host name or (if "no")
    76 - --              # that all package %{BUILDHOST} are not equal the host
    77 - --              # name
    78 - --              
    79 - --              | "Assertion-PackageNames:" 
    80 - --                ("!"?.mode-regex.":"."!"?.package-regex)+
    81 - --              # require all package %{NAME} to (not) match the
    82 - --              # specified regex while the current RPM run-time mode
    83 - --              # has to (not) match the specified regex. RPM run-time
    84 - --              # modes are: query, verify, checksig, resign, install,
    85 - --              # erase, build rebuild, recompile, tarbuild, initdb,
    86 - --              # rebuilddb and verifydb.
    87 - --              
    88 - --              | "Assertion-PackageReleaseAge:"
    89 - --                percent.":".duration.":".dist-regex ((package-name|"*").":".release)+
    90 - --              # require that for at least the specified amount (in
    91 - --              # percent) of packages, which %{DISTRIBUTION} matches
    92 - --              # the specified regex, the %{RELEASE} is at least as
    93 - --              # old as the specified release or at least not older
    94 - --              # than the specified duration.
    95 - --
    96 - --              | "Assertion-Expression:" expression
    97 - --              # evaluates the Lua boolean expression after expanding
    98 - --              # RPM macros %{VARNAME} and expanding the construct
    99 - --              # "<string>" ~~ /<regex>/ into the corresponding PCRE
   100 - --              # based regular expression match.
   101 - --  
   102 - --  version       ::= /^\d+\.\d+\.\d+$/
   103 - --  yes-no        ::= /^yes|no$/
   104 - --  url           ::= /^https?:\/\/.+$/
   105 - --  path          ::= /^\/.+$/
   106 - --  user          ::= /^[a-z][a-zA-Z0-9_]*$/
   107 - --  group         ::= /^[a-z][a-zA-Z0-9_]*$/
   108 - --  domain        ::= /^(?:[^.]+\.)+[^.]+$/
   109 - --  mode-regex    ::= /^.+$/
   110 - --  package-regex ::= /^.+$/
   111 - --  package-name  ::= /^[a-z][a-zA-Z0-9-]*$/
   112 - --  percent       ::= /^\d+%$/
   113 - --  duration      ::= /^\d+[smhdw]?$/
   114 - --  release       ::= /^\d{8}$/
   115 - --  iso-date      ::= /^\d{4}-\d{2}-\d{2}$/
   116 - --  expression    ::= /^.+$/
   118 - --  integrity processor version
   119 integrity.version = "1.0.0"
   121 - --  integrity processor validation callback function
   122 function integrity.validate(ctx, cfg)
   123     integrity.util.debug(1, "OpenPKG run-time license integrity validation")
   124     integrity.util.debug(4, function (ctx) return "dump: ctx = " .. util.dump(ctx) end, ctx)
   125     integrity.util.debug(4, function (cfg) return "dump: cfg = " .. util.dump(cfg) end, cfg)
   127     --  process "Assertion-OnlineApproval" constraint
   128     if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then
   129         --  support explicitly requested license exception
   130         cfg["Assertion-OnlineApproval"] = "http://openpkg.com/go/framework-license-exception"
   131     end
   132     if cfg["Assertion-OnlineApproval"] ~= nil then
   133         integrity.util.debug(2, "checking: Assertion-OnlineApproval: \"%s\"", cfg["Assertion-OnlineApproval"])
   134         local uuids = integrity.util.uuids()
   135         if  uuids["UUID_REGISTRY"] == "" then
   136             uuids["UUID_REGISTRY"] = "unknown"
   137         end
   138         if  uuids["UUID_INSTANCE"] == "" then
   139             uuids["UUID_INSTANCE"] = "unknown"
   140         end
   141         if  uuids["UUID_PLATFORM"] == "" then
   142             uuids["UUID_PLATFORM"] = "unknown"
   143         end
   144         local request = cfg["Assertion-OnlineApproval"]
   145         request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"]
   146         request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"]
   147         request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"]
   148         integrity.util.debug(3, "info: remote request \"%s\"", request)
   149         local response = rpm.expand("%(%{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "')")
   150         integrity.util.debug(3, "info: remote response \"%s\"", response)
   151         if util.rmatch(response, "(?s)^\\s*OK\\s*$") then
   152             --  approved
   153             if os.getenv("OPENPKG_LICENSE_EXCEPTION") ~= nil then
   154                 --  support explicitly requested license exception
   155                 cfg["Assertion-ErrorToWarning"] = "yes"
   156             end
   157         else
   158             --  rejected
   159             cfg["Assertion-ErrorToWarning"] = "no"
   160             return integrity.util.error(ctx, cfg,
   161                 "license requires online approval but we failed to get " ..
   162                 "an \"OK\" response from the online service")
   163         end
   164     end
   166     --  process "Assertion-MinProcVersion" constraint
   167     integrity.util.debug(2, "checking: Assertion-MinProcVersion: \"%s\"", cfg["Assertion-MinProcVersion"])
   168     if cfg["Assertion-MinProcVersion"] == nil then
   169         return integrity.util.error(ctx, cfg,
   170             "license configuration is missing required \"Assertion-MinProcVersion\" parameter")
   171     end
   172     integrity.util.debug(3, "require: %s <= %s", cfg["Assertion-MinProcVersion"], integrity.version)
   173     if rpm.vercmp(cfg["Assertion-MinProcVersion"], integrity.version) > 0 then
   174         return integrity.util.error(ctx, cfg,
   175             "license configuration requires a license processor of " ..
   176             "at least version \"" .. cfg["Assertion-MinProcVersion"] .. "\"")
   177     end
   179     --  process "Assertion-OnlineReporting" constraint
   180     if cfg["Assertion-OnlineReporting"] ~= nil then
   181         integrity.util.debug(2, "checking: Assertion-OnlineReporting: \"%s\"", cfg["Assertion-OnlineReporting"])
   182         local uuids = integrity.util.uuids()
   183         if  uuids["UUID_REGISTRY"] == "" then
   184             uuids["UUID_REGISTRY"] = "unknown"
   185         end
   186         if  uuids["UUID_INSTANCE"] == "" then
   187             uuids["UUID_INSTANCE"] = "unknown"
   188         end
   189         if  uuids["UUID_PLATFORM"] == "" then
   190             uuids["UUID_PLATFORM"] = "unknown"
   191         end
   192         local request = cfg["Assertion-OnlineReporting"]
   193         request = request .. "?UUID_REGISTRY=" .. uuids["UUID_REGISTRY"]
   194         request = request .. "&UUID_INSTANCE=" .. uuids["UUID_INSTANCE"]
   195         request = request .. "&UUID_PLATFORM=" .. uuids["UUID_PLATFORM"]
   196         integrity.util.debug(3, "info: remote request \"%s\"", request)
   197         rpm.expand("%(nohup %{l_prefix}/bin/openpkg curl -s -L -R '" .. request .. "' >/dev/null 2>&1 &)")
   198         integrity.util.debug(3, "response: (ignored, because asynchronous operation)")
   199     end
   201     --  process "Assertion-Prefix" constraint
   202     if cfg["Assertion-Prefix"] ~= nil then
   203         integrity.util.debug(2, "checking: Assertion-Prefix: \"%s\"", cfg["Assertion-Prefix"])
   204         local prefix = rpm.expand("%{l_prefix}")
   205         integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Prefix"], prefix)
   206         if cfg["Assertion-Prefix"] ~= prefix then
   207             return integrity.util.error(ctx, cfg,
   208                 "instance prefix \"" .. prefix .. "\" " ..
   209                 "does not match value \"" .. cfg["Assertion-Prefix"] .. "\" of " ..
   210                 "license configuration parameter \"Assertion-Prefix\"")
   211         end
   212     end
   214     --  process "Assertion-User" constraint
   215     if cfg["Assertion-User"] ~= nil then
   216         integrity.util.debug(2, "checking: Assertion-User: \"%s\"", cfg["Assertion-User"])
   217         local user = rpm.expand("%{l_musr}")
   218         integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-User"], user)
   219         if cfg["Assertion-User"] ~= user then
   220             return integrity.util.error(ctx, cfg,
   221                 "instance management user \"" .. user .. "\" " ..
   222                 "does not match value \"" .. cfg["Assertion-User"] .. "\" of " ..
   223                 "license configuration parameter \"Assertion-User\"")
   224         end
   225     end
   227     --  process "Assertion-Group" constraint
   228     if cfg["Assertion-Group"] ~= nil then
   229         integrity.util.debug(2, "checking: Assertion-Group: \"%s\"", cfg["Assertion-Group"])
   230         local group = rpm.expand("%{l_mgrp}")
   231         integrity.util.debug(3, "require: \"%s\" == \"%s\"", cfg["Assertion-Group"], group)
   232         if cfg["Assertion-Group"] ~= group then
   233             return integrity.util.error(ctx, cfg,
   234                 "instance management group \"" .. group .. "\" " ..
   235                 "does not match value \"" .. cfg["Assertion-Group"] .. "\" of " ..
   236                 "license configuration parameter \"Assertion-Group\"")
   237         end
   238     end
   240     --  process "Assertion-Domain" constraint
   241     if cfg["Assertion-Domain"] ~= nil then
   242         integrity.util.debug(2, "checking: Assertion-Domain: \"%s\"", cfg["Assertion-Domain"])
   243         local domain = rpm.expand("%(%{l_shtool} echo -n -e '%d')")
   244         integrity.util.debug(3, "require: \"%s\" ~~ /(?s)^.*%s$/", domain, cfg["Assertion-Domain"])
   245         local s, _, m = util.rmatch(domain, "(?s)^.*" .. cfg["Assertion-Domain"] .. "$")
   246         if s == nil then
   247             return integrity.util.error(ctx, cfg,
   248                 "host domain \"" .. domain .. "\" " ..
   249                 "does not end in pattern \"" .. cfg["Assertion-Domain"] .. "\") " ..
   250                 "of license configuration parameter \"Assertion-Domain\"")
   251         end
   252     end
   254     --  process "Assertion-LifeTime" constraint
   255     if cfg["Assertion-LifeTime"] ~= nil then
   256         integrity.util.debug(2, "checking: Assertion-LifeTime: \"%s\"", cfg["Assertion-LifeTime"])
   258         --  determine lifetime begin and end
   259         local lifetime = cfg["Assertion-LifeTime"]
   260         local s, _, m = util.rmatch(lifetime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$")
   261         if s == nil then
   262             return integrity.util.error(ctx, cfg,
   263                 "failed to extract time information from " ..
   264                 "license configuration parameter \"Assertion-LifeTime\"")
   265         end
   266         local lifetime_begin = os.time({
   267             year  = tonumber(m[1]),
   268             month = tonumber(m[2]),
   269             day   = tonumber(m[3]),
   270             hour  = 0,
   271             min   = 0,
   272             sec   = 0
   273         })
   274         local lifetime_end = os.time({
   275             year  = tonumber(m[4]),
   276             month = tonumber(m[5]),
   277             day   = tonumber(m[6]),
   278             hour  = 23,
   279             min   = 59,
   280             sec   = 59
   281         })
   283         --  check whether current run-time is within lifetime
   284         local t_now = os.time()
   285         integrity.util.debug(3, "require: %d <= %d <= %d", lifetime_begin, t_now, lifetime_end)
   286         if not (lifetime_begin <= t_now and t_now <= lifetime_end) then
   287             return integrity.util.error(ctx, cfg,
   288                 "current time \"" .. os.date("!%Y-%m-%d %H:%M:%S UTC", t_now) .. "\" " ..
   289                 "is not within the timerange \"" .. cfg["Assertion-LifeTime"] .. "\" " ..
   290                 "of license configuration parameter \"Assertion-LifeTime\"")
   291         end
   292     end
   294     --  process "Assertion-GrantTime" constraint
   295     if cfg["Assertion-GrantTime"] ~= nil then
   296         integrity.util.debug(2, "checking: Assertion-GrantTime: \"%s\"", cfg["Assertion-GrantTime"])
   298         --  determine granttime begin and end
   299         local granttime = cfg["Assertion-GrantTime"]
   300         local s, _, m = util.rmatch(granttime, "^(?s)(\\d{4})-(\\d{2})-(\\d{2})\\s*:\\s*(\\d{4})-(\\d{2})-(\\d{2})$")
   301         if s == nil then
   302             return integrity.util.error(ctx, cfg,
   303                 "failed to extract time information from " ..
   304                 "license configuration parameter \"Assertion-GrantTime\"")
   305         end
   306         local granttime_begin = os.time({
   307             year  = tonumber(m[1]),
   308             month = tonumber(m[2]),
   309             day   = tonumber(m[3]),
   310             hour  = 0,
   311             min   = 0,
   312             sec   = 0
   313         })
   314         local granttime_end = os.time({
   315             year  = tonumber(m[4]),
   316             month = tonumber(m[5]),
   317             day   = tonumber(m[6]),
   318             hour  = 23,
   319             min   = 59,
   320             sec   = 59
   321         })
   323         --  determine OpenPKG Framework release time
   324         --  (allow openpkg.spec:%pre to override with a higher value for pre-checking)
   325         local t_release = 0
   326         local result = {}
   327         for _, line in ipairs(rpm.query("Q:%{RELEASE}", false, "openpkg")) do
   328             local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   329             if s ~= nil then
   330                 table.insert(result, m[1])
   331             end
   332         end
   333         if result[1] ~= nil then
   334             local s, _, m = util.rmatch(result[1], "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   335             if s ~= nil then
   336                 t_release = os.time({
   337                     year  = tonumber(m[1]),
   338                     month = tonumber(m[2]),
   339                     day   = tonumber(m[3]),
   340                     hour  = 0,
   341                     min   = 0,
   342                     sec   = 0
   343                 })
   344             end
   345         end
   346         if t_release == 0 then
   347             return integrity.util.error(ctx, cfg,
   348                 "failed to determine OpenPKG Framework release time")
   349         end
   350         local override = os.getenv("OPENPKG_FRAMEWORK_RELEASE")
   351         if override ~= nil then
   352             local s, _, m = util.rmatch(override, "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   353             if s ~= nil then
   354                 local t_override = os.time({
   355                     year  = tonumber(m[1]),
   356                     month = tonumber(m[2]),
   357                     day   = tonumber(m[3]),
   358                     hour  = 0,
   359                     min   = 0,
   360                     sec   = 0
   361                 })
   362                 if t_release < t_override then
   363                     t_release = t_override
   364                 end
   365             end
   366         end
   368         --  check whether current OpenPKG Framework release time is within granttime
   369         integrity.util.debug(3, "require: %d <= %d <= %d", granttime_begin, t_release, granttime_end)
   370         if not (granttime_begin <= t_release and t_release <= granttime_end) then
   371             return integrity.util.error(ctx, cfg,
   372                 "current OpenPKG Framework release time \"" .. os.date("%Y-%m-%d", t_release) .. "\" " ..
   373                 "is not within the timerange \"" .. cfg["Assertion-GrantTime"] .. "\" " ..
   374                 "of license configuration parameter \"Assertion-GrantTime\"")
   375         end
   376     end
   378     --  process "Assertion-InstanceAge" constraint
   379     if cfg["Assertion-InstanceAge"] ~= nil then
   380         integrity.util.debug(2, "checking: Assertion-InstanceAge: \"%s\"", cfg["Assertion-InstanceAge"])
   382         --  determine maximum instance age in seconds
   383         local t_diff_max = cfg["Assertion-InstanceAge"]
   384         t_diff_max = 0 + util.rsubst(t_diff_max, "^(\\d+)([smhdw])$", function (t, unit)
   385             if     unit == "s" then t = t * 1
   386             elseif unit == "m" then t = t * 60
   387             elseif unit == "h" then t = t * 60 * 60
   388             elseif unit == "d" then t = t * 60 * 60 * 24
   389             elseif unit == "w" then t = t * 60 * 60 * 24 * 7
   390             end
   391             return t
   392         end)
   394         --  approach 1: determine install time via timestamp of UUID_REGISTRY
   395         local uuids = integrity.util.uuids()
   396         if uuids["UUID_REGISTRY"] == "" then
   397             return integrity.util.error(ctx, cfg,
   398                 "failed to load UUID_REGISTRY")
   399         end
   400         txt = uuid.describe(uuids["UUID_REGISTRY"])
   401         if txt == nil then
   402             return integrity.util.error(ctx, cfg,
   403                 "failed to parse extracted UUID_REGISTRY string \"" .. uuids["UUID_REGISTRY"] .. "\" as an UUID")
   404         end
   405         local s, _, m = util.rmatch(txt, "(?s)^.*time:\\s+(\\d{4})-(\\d{2})-(\\d{2})\\s+(\\d{2}):(\\d{2}):(\\d{2}).*$")
   406         if s == nil then
   407             return integrity.util.error(ctx, cfg,
   408                 "failed to extract timestamp from UUID_REGISTRY \"" .. uuids["UUID_REGISTRY"] .. "\"")
   409         end
   410         local t_install = os.time({
   411             year  = tonumber(m[1]),
   412             month = tonumber(m[2]),
   413             day   = tonumber(m[3]),
   414             hour  = tonumber(m[4]),
   415             min   = tonumber(m[5]),
   416             sec   = tonumber(m[6])
   417         })
   419         --  approach 2: determine install time via first install time of "openpkg" package
   420         local result = {}
   421         for _, line in ipairs(rpm.query(
   422             "Q:%|ORIGINTIME?{" ..
   423                 "%{ORIGINTIME}" ..       -- regular case: RPM 5 installed/updated with RPM 5
   424             "}:{" ..
   425                 "%|INSTALLTIME?{" ..
   426                     "%{INSTALLTIME}" ..  -- special case: RPM 5 installed initially with RPM 4
   427                 "}:{" ..
   428                 "}|" ..
   429             "}|", false, "openpkg"
   430         )) do
   431             local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   432             if s ~= nil then
   433                 table.insert(result, m[1])
   434             end
   435         end
   436         if result[1] ~= nil then
   437             local n = tonumber(result[1])
   438             if n > 0 then
   439                 t_install = n
   440             end
   441         end
   443         --  check time difference
   444         local t_now = os.time()
   445         local t_diff = os.difftime(t_now, t_install)
   446         integrity.util.debug(3, "calc: %d - %d = %d", t_now, t_install, t_diff)
   447         if t_diff < 0 then
   448             return integrity.util.error(ctx, cfg,
   449                 "current system time \"" .. t_now .. "\" is lower than " ..
   450                 "instance installation time \"" .. t_install .. "\"")
   451         end
   452         integrity.util.debug(3, "require: %d <= %d", t_diff, t_diff_max)
   453         if t_diff > t_diff_max then
   454             return integrity.util.error(ctx, cfg,
   455                 "instance age \"" .. t_diff .. "\" " ..
   456                 "is greater than value \"" .. t_diff_max .. "\" (\"" .. cfg["Assertion-InstanceAge"] .. "\") " ..
   457                 "of license configuration parameter \"Assertion-InstanceAge\"")
   458         end
   459     end
   461     --  process "Assertion-FromSourceOnTarget" constraint
   462     if cfg["Assertion-FromSourceOnTarget"] ~= nil then
   463         integrity.util.debug(2, "checking: Assertion-FromSourceOnTarget: \"%s\"", cfg["Assertion-FromSourceOnTarget"])
   464         local hostname = rpm.hostname()
   465         for _, line in ipairs(rpm.query("Q:%{NAME}:%{BUILDHOST}", true, "*")) do
   466             local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(.+)$")
   467             if s ~= nil then
   468                 local name = m[1]
   469                 local buildhost = m[2]
   470                 integrity.util.debug(4, "info: name \"%s\", buildhost \"%s\"", name, buildhost)
   471                 if not util.rmatch(name, "(?s)^gpg-.+$") and buildhost ~= "localhost" then
   472                     if cfg["Assertion-FromSourceOnTarget"] == "yes" and buildhost ~= hostname then
   473                         return integrity.util.error(ctx, cfg,
   474                             "license-required \"build from source on target system only\" situation not met because " ..
   475                             "package build host \"" .. buildhost .. "\" is not(!) equal to the package install host \"" .. hostname .. "\".")
   476                     end
   477                     if cfg["Assertion-FromSourceOnTarget"] == "no" and buildhost == hostname then
   478                         return integrity.util.error(ctx, cfg,
   479                             "license-required \"build binaries on separate build-host only\" situation not met because " ..
   480                             "package build host \"" .. buildhost .. "\" is equal to the package install host \"" .. hostname .. "\".")
   481                     end
   482                 end
   483             end
   484         end
   485     end
   487     --  process "Assertion-PackageNames" constraints
   488     if cfg["Assertion-PackageNames"] ~= nil then
   489         integrity.util.debug(2, "checking: Assertion-PackageNames: \"%s\"", cfg["Assertion-PackageNames"])
   491         --  query RPMDB for names of all installed packages
   492         local packages = {}
   493         for _, line in ipairs(rpm.query("Q:%{NAME}", true, "*")) do
   494             local s, _, m = util.rmatch(line, "(?s)^Q:(.+)$")
   495             if s ~= nil then
   496                 table.insert(packages, m[1])
   497             end
   498         end
   500         --  iterate over all constraints
   501         for _, constraint in
   502             ipairs(
   503                 util.rsplit(
   504                     util.rsubst(
   505                         cfg["Assertion-PackageNames"],
   506                         "(?s)^\\s*(.+?)\\s*$", "%1"
   507                     ),
   508                     "(?s)\\s+"
   509                 )
   510             ) do
   511             --  parse constraint
   512             local s, _, m = util.rmatch(constraint, "(?s)^(!?)([^:]+):(!?)(.+)$")
   513             if s == nil then
   514                 return integrity.util.error(ctx, cfg,
   515                     "invalid syntax in license configuration \"Assertion-PackageNames\" " ..
   516                     "parameter: \"" ..  constraint .. "\"")
   517             end
   518             local mode_negate    = m[1] ~= ""
   519             local mode_regex     = m[2]
   520             local package_negate = m[3] ~= ""
   521             local package_regex  = m[4]
   522             --  apply the mode filter
   523             local mode_matches, _, _ = util.rmatch(ctx.rpm.mode, mode_regex);
   524             if     (not mode_negate and mode_matches ~= nil)
   525                 or (    mode_negate and mode_matches == nil) then
   526                 --  apply the package filter to names of all installed packages
   527                 for _, package in ipairs(packages) do
   528                     if package_negate then
   529                         integrity.util.debug(3, "require: \"%s\" !~ /%s/", package, package_regex)
   530                     else
   531                         integrity.util.debug(3, "require: \"%s\" ~~ /%s/", package, package_regex)
   532                     end
   533                     local package_matches, _, _ = util.rmatch(package, package_regex)
   534                     if  not (   (not package_negate and package_matches ~= nil)
   535                              or (    package_negate and package_matches == nil)) then
   536                         --  indicate integrity validation error
   537                         return integrity.util.error(ctx, cfg,
   538                             "installed package \"" .. package .. "\" " ..
   539                             "under RPM run-time mode \"" .. ctx.rpm.mode .. "\" " ..
   540                             "not covered by pattern \"" .. package_regex .. "\" " ..
   541                             "of license configuration parameter \"Assertion-PackageNames\"")
   542                     end
   543                 end
   544             end
   545         end
   546     end
   548     --  process "Assertion-PackageReleaseAge"
   549     if cfg["Assertion-PackageReleaseAge"] ~= nil then
   550         integrity.util.debug(2, "checking: Assertion-PackageReleaseAge: \"%s[...]\"", string.sub(cfg["Assertion-PackageReleaseAge"], 1, 20))
   552         --  parse constraint
   553         local constraint = cfg["Assertion-PackageReleaseAge"]
   554         local s, _, m = util.rmatch(constraint, "(?s)^([^:]+)%:([^:]+):([^\\s]+)\\s+(.+)$")
   555         if s == nil then
   556             return integrity.util.error(ctx, cfg,
   557                 "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" parameter")
   558         end
   559         local percent   = m[1] / 100
   560         local offset    = m[2]
   561         local distregex = m[3]
   562         local spec      = m[4]
   564         --  determine maximum release time difference (in seconds)
   565         local t_diff_max = 0 + util.rsubst(offset, "^(\\d+)([smhdw])$", function (t, unit)
   566             if     unit == "s" then t = t * 1
   567             elseif unit == "m" then t = t * 60
   568             elseif unit == "h" then t = t * 60 * 60
   569             elseif unit == "d" then t = t * 60 * 60 * 24
   570             elseif unit == "w" then t = t * 60 * 60 * 24 * 7
   571             end
   572             return t
   573         end)
   575         --  iterate over all package specifications to build release map
   576         local releases = {}
   577         for _, constraint in
   578             ipairs(
   579                 util.rsplit(
   580                     util.rsubst(
   581                         spec,
   582                         "(?s)^\\s*(.+?)\\s*$", "%1"
   583                     ),
   584                     "(?s)\\s+"
   585                 )
   586             ) do
   588             --  parse specification into package name and release constraint
   589             local s, _, m = util.rmatch(constraint, "(?s)^([^:]+):(.+)$")
   590             if s == nil then
   591                 return integrity.util.error(ctx, cfg,
   592                     "invalid syntax in license configuration \"Assertion-PackageReleaseAge\" " ..
   593                     "parameter: \"" ..  constraint .. "\"")
   594             end
   596             -- store result into release map
   597             releases[m[1]] = m[2]
   598         end
   600         --  query RPMDB for releases of all installed packages and decide
   601         --  whether the release time is inside or outside our constraint window
   602         local release_window_inside  = 0
   603         local release_window_outside = 0
   604         local release_window_foreign = 0
   605         local release_window_unknown = 0
   606         for _, line in ipairs(rpm.query("Q:%{NAME}:%{RELEASE}:%{DISTRIBUTION}", true, "*")) do
   607             local s, _, m = util.rmatch(line, "(?s)^Q:([^:]+):(\\d\\d\\d\\d)(\\d\\d)(\\d\\d):(.+)$")
   608             if s ~= nil then
   609                 --  parse query results
   610                 local name = m[1]
   611                 local t_release = os.time({
   612                     year  = tonumber(m[2]),
   613                     month = tonumber(m[3]),
   614                     day   = tonumber(m[4]),
   615                     hour  = 23,
   616                     min   = 59,
   617                     sec   = 59
   618                 })
   619                 local dist = m[5]
   621                 --  only check files of the constrained distribution(s)
   622                 if util.rmatch(dist, "(?s)" .. distregex) then
   624                     --  determine minimum release constraint
   625                     local t_release_min = releases[name]
   626                     if t_release_min == nil then
   627                         t_release_min = releases["*"]
   628                     end
   629                     if t_release_min == nil then
   630                         t_release_min = os.time()
   631                     else
   632                         local s, _, m = util.rmatch(t_release_min, "^(?s)(\\d{4})(\\d{2})(\\d{2})$")
   633                         t_release_min = os.time({
   634                             year  = tonumber(m[1]),
   635                             month = tonumber(m[2]),
   636                             day   = tonumber(m[3]),
   637                             hour  = 0,
   638                             min   = 0,
   639                             sec   = 0
   640                         })
   641                     end
   643                     --  check time difference of package release
   644                     local t_diff = os.difftime(t_release_min, t_release)
   645                     integrity.util.debug(4, "calc: %d - %d = %d", t_release_min, t_release, t_diff)
   646                     integrity.util.debug(4, "require: %d <= 0 or (%d > 0 and %d < %d)", t_diff, t_diff, t_diff, t_diff_max)
   647                     if t_diff <= 0 or (t_diff > 0 and t_diff < t_diff_max) then
   648                         release_window_inside  = release_window_inside  + 1
   649                     else
   650                         release_window_outside = release_window_outside + 1
   651                     end
   652                 else
   653                     release_window_foreign = release_window_foreign + 1
   654                 end
   655             else
   656                 release_window_unknown = release_window_unknown + 1
   657             end
   658         end
   659         integrity.util.debug(3, "info: inside %d, outside %d, foreign %d, unknown %d",
   660             release_window_inside, release_window_outside, release_window_foreign, release_window_unknown)
   662         --  check validity of overall constraint
   663         local percent_inside =
   664             (release_window_inside / (release_window_inside + release_window_outside))
   665         integrity.util.debug(3, "require: %d >= %d", percent_inside, percent)
   666         if percent_inside < percent then
   667             return integrity.util.error(ctx, cfg,
   668                 "there are only " .. math.floor(percent_inside * 100) .. "% " ..
   669                 "packages inside the release date constraint " ..
   670                 "(expected a minimum of " .. math.floor(percent * 100) .. "%)")
   671         end
   672     end
   674     --  process "Assertion-Expression" constraint
   675     if cfg["Assertion-Expression"] ~= nil then
   676         integrity.util.debug(2, "checking: Assertion-Expression: \"%s\"", cfg["Assertion-Expression"])
   678         --  expand special consytructs in expression
   679         local expr = cfg["Assertion-Expression"]
   680         expr = util.rsubst(expr, "(%\{[a-zA-Z_][a-zA-Z0-9_]+\})", function (str)
   681             return rpm.expand(str)
   682         end)
   683         expr = util.rsubst(expr, "\"((?:\\\\.|[^\"])+)\"\\s*~~\\s*/((?:\\\\.|[^/])+)/", function (str, regex)
   684             if util.rmatch(str, "(?s)" .. regex) ~= nil then
   685                 return "true"
   686             else
   687                 return "false"
   688             end
   689         end)
   691         --  evaluate expression
   692         integrity.util.debug(3, "evaluate: %s", expr)
   693         result = assert(loadstring(expr))()
   694         if type(result) ~= "boolean" then
   695             result = false
   696         end
   697         if not result then
   698             return integrity.util.error(ctx, cfg,
   699                 "expression \"" .. cfg["Assertion-Expression"] .. "\" " ..
   700                 "of license configuration parameter \"Assertion-Expression\"" ..
   701                 "evaluated to false")
   702         end
   703     end
   705     --  indicate license integrity validation success
   706     return "OK"
   707 end
   709 - --  integrity processor utilities namespace
   710 integrity.util = {}
   712 - --  write debug information to stderr
   713 function integrity.util.debug(level_this, msg, ...)
   714     local level_min = os.getenv("OPENPKG_LICENSE_DEBUG")
   715     if level_min ~= nil then
   716         if type(level_min) == "string" then
   717             level_min = tonumber(level_min)
   718         end
   719         if type(level_min) ~= "number" then
   720             level_min = 0
   721         end
   722         if level_this <= level_min then
   723             local output
   724             if type(msg) == "function" then
   725                 output = msg(...)
   726             else
   727                 output = string.format(msg, ...)
   728             end
   729             local prefix = ""
   730             local i = 1
   731             while (i < level_this) do
   732                 prefix = prefix .. "    "
   733                 i = i + 1
   734             end
   735             io.stderr:write("rpm: DEBUG: " .. prefix .. output .. "\n")
   736         end
   737     end
   738 end
   740 - --  load OpenPKG instance UUIDs
   741 function integrity.util.uuids()
   742     local uuids = {
   743         UUID_REGISTRY = "",
   744         UUID_INSTANCE = "",
   745         UUID_PLATFORM = ""
   746     }
   747     local filename = rpm.expand("%{l_prefix}/etc/openpkg/uuid")
   748     local txt = rpm.slurp(filename)
   749     if txt ~= nil then
   750         for name, _ in pairs(uuids) do
   751             local s, _, m = util.rmatch(txt, "(?s)^.*" .. name .. "=\"([^\"\"]+)\".*$")
   752             if s ~= nil then
   753                 uuids[name] = m[1]
   754             end
   755         end
   756     end
   757     return uuids
   758 end
   760 - --  report validation warning
   761 function integrity.util.warning(ctx, cfg, warning)
   762     --  return prominent warning message
   763     return
   764         "WARNING: OpenPKG run-time license check failed -- continue processing\n" ..
   765         "+-----------------------------------------------------------------------------+\n" ..
   766         "| Attention, the OpenPKG RPM run-time integrity checking facility encountered a\n" ..
   767         "| non-fatal problem during license checking, but allows processing to continue.\n" ..
   768         "| The particular warning reported by the OpenPKG license processor is:\n" ..
   769         "|\n" ..
   770         util.textwrap("|   ", warning, 60, 70) ..
   771         "|\n" ..
   772         "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" ..
   773         "| Go to http://openpkg.com/go/framework-license for more details, please.\n" ..
   774         "+-----------------------------------------------------------------------------+"
   775 end
   777 - --  report validation error
   778 function integrity.util.error(ctx, cfg, error)
   779     --  support conversion of errors into warnings
   780     if cfg["Assertion-ErrorToWarning"] ~= nil then
   781         if cfg["Assertion-ErrorToWarning"] == "yes" then
   782             return integrity.util.warning(ctx, cfg, error)
   783         end
   784     end
   786     --  return prominent error message
   787     return
   788         "ERROR: OpenPKG run-time license check failed -- stopping processing\n" ..
   789         "+-----------------------------------------------------------------------------+\n" ..
   790         "| Sorry, the OpenPKG RPM run-time integrity checking facility encountered a\n" ..
   791         "| fatal problem during license checking and stops processing immediately.\n" ..
   792         "| The particular error reported by the OpenPKG license processor is:\n" ..
   793         "|\n" ..
   794         util.textwrap("|   ", error, 60, 70) ..
   795         "|\n" ..
   796         "| Notice: Operation of the OpenPKG Framework requires a valid license.\n" ..
   797         "| Go to http://openpkg.com/go/framework-license for more details, please.\n" ..
   798         "| Run \"openpkg man license\" for details about local license management.\n" ..
   799         "+-----------------------------------------------------------------------------+"
   800 end
   802 -----BEGIN PGP SIGNATURE-----
   803 Comment: OpenPKG GmbH <openpkg@openpkg.com>
   805 iEYEARECAAYFAk8BelcACgkQZwQuyWG3rjTL6QCeLTLVj4PTnd/E7mf+Sv4mgbZj
   806 5J0AoMXrO4EimPSSCZSJ1TLW8f8GP+B5
   807 =AVpf
   808 -----END PGP SIGNATURE-----

mercurial