Access.lua 19.8 KB
Newer Older
1
local _G = require "_G"
2
local assert = _G.assert
3
local ipairs = _G.ipairs
4
local pairs = _G.pairs
5
local pcall = _G.pcall
6
local rawget = _G.rawget
7
local select = _G.select
8
local setmetatable = _G.setmetatable
9
local type = _G.type
10 11 12

local array = require "table"
local unpack = array.unpack or _G.unpack
13

14 15
local coroutine = require "coroutine"
local running = coroutine.running
16 17 18 19 20 21 22 23 24 25 26 27 28

local string = require "string"
local char = string.char

local math = require "math"
local random = math.random
local randomseed = math.randomseed

local struct = require "struct"
local encode = struct.pack

local socket = require "socket.core"
local gettime = socket.gettime
29 30 31 32

local table = require "loop.table"
local clear = table.clear
local copy = table.copy
33
local memoize = table.memoize
34 35 36

local oil = require "oil"
local neworb = oil.init
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
37
local CORBAException = require "oil.corba.giop.Exception"
38 39 40 41 42
local idl = require "oil.corba.idl"
local OctetSeq = idl.OctetSeq

local hash = require "lce.hash"
local sha256 = hash.sha256
43

44 45
local LRUCache = require "loop.collection.LRUCache"

46 47 48
local log = require "openbus.util.logger"
local oo = require "openbus.util.oo"
local class = oo.class
49 50
local sysex = require "openbus.util.sysex"
local is_NO_PERMISSION = sysex.is_NO_PERMISSION
51
local tickets = require "openbus.util.tickets"
52 53 54 55

local msg = require "openbus.core.messages"
local idl = require "openbus.core.idl"
local loadidl = idl.loadto
56
local InvalidLoginsException = idl.types.services.access_control.InvalidLogins
57
local EncryptedBlockSize = idl.const.EncryptedBlockSize
58
local CredentialContextId = idl.const.credential.CredentialContextId
59
local loginconst = idl.const.services.access_control
60 61 62 63 64 65
local InvalidChainCode = loginconst.InvalidChainCode
local InvalidCredentialCode = loginconst.InvalidCredentialCode
local InvalidLoginCode = loginconst.InvalidLoginCode
local InvalidPublicKeyCode = loginconst.InvalidPublicKeyCode
local InvalidRemoteCode = loginconst.InvalidRemoteCode
local InvalidTargetCode = loginconst.InvalidTargetCode
66
local UnknownBusCode = loginconst.UnknownBusCode
67 68
local NoLoginCode = loginconst.NoLoginCode
local UnavailableBusCode = loginconst.UnavailableBusCode
69 70
local oldidl = require "openbus.core.legacy.idl"
local loadoldidl = oldidl.loadto
71 72 73
local oldconst = oldidl.const.v2_0
local oldtypes = oldidl.types.v2_0
local LegacyCredentialContextId = oldconst.credential.CredentialContextId
74

75 76
assert(EncryptedBlockSize == oldconst.EncryptedBlockSize)
assert(idl.const.HashValueSize == oldconst.HashValueSize)
77

78
local repids = {
79 80 81
  CallChain = idl.types.services.access_control.CallChain,
  CredentialData = idl.types.credential.CredentialData,
  CredentialReset = idl.types.credential.CredentialReset,
82 83 84
  LegacyCallChain = oldtypes.services.access_control.CallChain,
  LegacyCredentialData = oldtypes.credential.CredentialData,
  LegacyCredentialReset = oldtypes.credential.CredentialReset,
85
}
86 87
local VersionHeader = char(idl.const.MajorVersion,
                           idl.const.MinorVersion)
88 89
local LegacyVersionHeader = char(oldconst.MajorVersion,
                                 oldconst.MinorVersion)
90 91
local SecretSize = 16

92
local NullChar = "\0"
93 94
local NullSecret = NullChar:rep(SecretSize)
local NullHash = NullChar:rep(idl.const.HashValueSize)
95
local NullChain = {
96
  encoded = "",
97
  signature = NullChar:rep(EncryptedBlockSize),
98 99
  originators = {},
  caller = nil,
100
}
101 102

local WeakKeys = {__mode = "k"}
103 104


105 106


107
local function calculateHash(version, secret, ticket, request)
108
  return sha256(encode(
109
    "<c2c0I4c0",             -- '<' flag to set to little endian
110
    version,                 -- 'c2' sequence of exactly 2 chars of a string
111
    secret,                  -- 'c0' sequence of all chars of a string
112 113
    ticket,                  -- 'I4' unsigned integer with 4 bytes
    request.operation_name)) -- 'c0' sequence of all chars of a string
114 115 116 117
end

randomseed(gettime())
local function newSecret()
118
  local bytes = {}
119
  for i=1, SecretSize do bytes[i] = random(0, 255) end
120
  return char(unpack(bytes))
121 122 123
end

local function setNoPermSysEx(request, minor)
124
  request.islocal = true
125
  request.success = false
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
126
  request.results = {CORBAException{"NO_PERMISSION",
127 128 129
    completed = "COMPLETED_NO",
    minor = minor,
  }}
130 131
end

132
local function validateCredential(self, credential, login, request)
133 134 135 136 137
  local chain = credential.chain
  if chain == nil or chain.target == self.login.entity then
    -- get current credential session
    local session = self.incomingSessions:rawget(credential.session)
    if session ~= nil then
138 139
      local version = credential.islegacy and LegacyVersionHeader
                                           or VersionHeader
140 141 142
      local ticket = credential.ticket
      -- validate credential data with session data
      if login.id == session.login
143
      and credential.hash == calculateHash(version, session.secret, ticket, request)
144 145
      and session.tickets:check(ticket) then
        return true
146 147 148 149 150
      end
    end
  end
  -- create a new session for the invalid credential
  return false
151 152
end

153
local function validateChain(self, chain, caller)
154
  if chain ~= nil then
155 156
    return chain.busid == self.busid
       and chain.caller.id == caller.id
157 158
       and( chain.signature == nil or -- hack for 'openbus.core.services.Access'
            self.buskey:verify(sha256(chain.encoded), chain.signature) )
159
  end
160
  return false
161 162
end

163 164


165 166
local Context = class()

167 168 169 170 171 172
-- Fields that must be provided before using the context:
-- orb: OiL ORB to be used to access the bus

-- Optional field that may be provided to configure the interceptor:
-- prvkey: default private key to be used by all connections using this context

173
function Context:__init()
174 175 176 177 178 179 180
  local orb = self.orb
  local types = orb.types
  local idltypes = {}
  for name, repid in pairs(repids) do
    idltypes[name] = types:lookup_id(repid)
  end
  self.types = idltypes
181 182 183 184 185 186 187 188 189 190
  self.callerChainOf = setmetatable({}, WeakKeys) -- [thread] = chain
  self.joinedChainOf = setmetatable({}, WeakKeys) -- [thread] = chain
end

function Context:getCallerChain()
  return self.callerChainOf[running()]
end

function Context:joinChain(chain)
  local thread = running()
191 192 193 194
  local joinedChainOf = self.joinedChainOf
  local old = joinedChainOf[thread]
  joinedChainOf[thread] = chain or self.callerChainOf[thread] -- or error?
  return old
195 196 197
end

function Context:exitChain()
198 199 200 201 202
  local thread = running()
  local joinedChainOf = self.joinedChainOf
  local old = joinedChainOf[thread]
  joinedChainOf[thread] = nil
  return old
203 204 205 206 207 208 209 210
end

function Context:getJoinedChain()
  return self.joinedChainOf[running()]
end



211
local Interceptor = class()
212

213
-- Fields that must be provided before using the interceptor:
214
-- context      : context object to be used to retrieve credential info from
215 216 217
-- busid        : UUID of the bus being accessed
-- buskey       : public key of the bus being accessed
-- AccessControl: AccessControl facet of the bus being accessed
218
-- LoginRegistry: LoginRegistry facet of the bus being accessed
219
-- login        : information about the login used to access the bus
220

221
-- Optional field that may be provided to configure the interceptor:
222
-- prvkey: private key associated to the key registered to the login
223

224
function Interceptor:__init()
225
  self:resetCaches()
226 227 228
end

function Interceptor:resetCaches()
229
  self.profile2login = LRUCache() -- [iop_profile] = loginid
230 231 232 233 234 235 236 237 238 239
  self.outgoingSessions = LRUCache()
  self.incomingSessions = LRUCache{
    retrieve = function(id)
      return {
        id = id,
        secret = newSecret(),
        tickets = tickets(),
      }
    end,
  }
240 241
end

242
function Interceptor:unmarshalSignedChain(chain, idltype)
243 244 245 246 247 248
  local encoded = chain.encoded
  if encoded ~= "" then
    local context = self.context
    local types = context.types
    local orb = context.orb
    local decoder = orb:newdecoder(chain.encoded)
249
    local decoded = decoder:get(idltype)
250 251 252 253 254
    local originators = decoded.originators
    originators.n = nil -- remove field 'n' created by OiL unmarshal
    chain.originators = originators
    chain.caller = decoded.caller
    chain.target = decoded.target
255
    chain.busid = decoded.bus
256 257 258 259 260
    return chain
  end
end

local unmarshalSignedChain = Interceptor.unmarshalSignedChain
261
function Interceptor:unmarshalCredential(contexts)
262 263 264
  local context = self.context
  local types = context.types
  local orb = context.orb
265
  local credential
266 267
  local data = contexts[CredentialContextId]
  if data ~= nil then
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    credential = orb:newdecoder(data):get(types.CredentialData)
    credential.chain = unmarshalSignedChain(self, credential.chain,
                                                  types.CallChain)
  elseif self.legacy ~= nil then
    data = contexts[LegacyCredentialContextId]
    if data ~= nil then
      credential = orb:newdecoder(data):get(types.LegacyCredentialData)
      credential.islegacy = true
      credential.chain = unmarshalSignedChain(self, credential.chain,
                                                    types.LegacyCallChain)
      if credential.chain ~= nil then
        credential.chain.busid = credential.bus
        credential.chain.islegacy = true
      end
    end
283
  end
284
  return credential
285 286
end

287 288 289


function Interceptor:sendrequest(request)
290
  local contexts = {}
291 292
  local context = self.context
  local orb = context.orb
293 294
  local sessionid, ticket, hash, signed = 0, 0, NullHash, NullChain
  local idltype, contextid
295 296
  local profile2login = self.profile2login
  local target = profile2login:get(request.profile_data)
297
  if target ~= nil then
298 299
    local session = self.outgoingSessions:get(target)
    if session ~= nil then -- credential session is established
300
      local chain = context.joinedChainOf[running()] or NullChain
301
      local entity = session.entity
302 303 304 305 306 307 308 309 310 311 312 313
      local ok, result, hashheader
      local islegacy = session.islegacy
      if islegacy then
        local legacy = self.legacy
        ok, result = pcall(legacy.signChainFor, legacy, target, chain)
        if not ok then
          if result._repid == InvalidLoginsException then
            for profile_data, profile_target in pairs(profile2login.map) do
              if target == profile_target then
                profile2login:remove(profile_data)
              end
            end
314
            setNoPermSysEx(request, InvalidTargetCode)
315 316 317
            return
          end
        end
318 319 320 321 322 323 324 325
      elseif chain.islegacy and self.legacy == nil then
        log:exception(msg.LegacyChainNotAllowed:tag{
          target = target,
          entity = entity,
          chain = chain,
        })
        setNoPermSysEx(request, InvalidChainCode)
        return
326 327 328
      else
        ok, result, islegacy = pcall(self.signChainFor, self, entity, chain)
      end
329 330 331 332 333
      if not ok then
        log:exception(msg.UnableToSignChainForTarget:tag{
          error = result,
          target = target,
          entity = entity,
334
          chain = (chain~=NullChain) and chain or nil,
335
        })
336
        setNoPermSysEx(request, UnavailableBusCode)
337 338
        return
      end
339 340 341 342 343 344 345 346 347 348
      if islegacy then
        idltype = context.types.LegacyCredentialData
        contextid = LegacyCredentialContextId
        hashheader = LegacyVersionHeader
      else
        idltype = context.types.CredentialData
        contextid = CredentialContextId
        hashheader = VersionHeader
      end
      signed = result
349 350 351
      sessionid = session.id
      ticket = session.ticket+1
      session.ticket = ticket
352
      hash = calculateHash(hashheader, session.secret, ticket, request)
353
      log:access(self, msg.PerformBusCall:tag{
354
        operation = request.operation_name,
355
        remote = target,
356
        islegacy = session.islegacy,
357 358
      })
    else
359 360 361
      log:access(self, msg.ReinitiateCredentialSession:tag{
        operation = request.operation_name,
      })
362
    end
363 364 365 366
  else
    log:access(self, msg.InitiateCredentialSession:tag{
      operation = request.operation_name,
    })
367
  end
368 369 370 371 372 373 374
  -- marshal credential information
  local credential = {
    bus = self.busid,
    login = self.login.id,
    session = sessionid,
    ticket = ticket,
    hash = hash,
375
    chain = signed,
376 377
  }
  local encoder = orb:newencoder()
378 379 380 381 382
  encoder:put(credential, idltype or context.types.CredentialData)
  contexts[contextid or CredentialContextId] = encoder:getdata()
  if contextid == nil and self.legacy ~= nil then
    contexts[LegacyCredentialContextId] = contexts[CredentialContextId]
  end
383
  request.service_context = contexts
384 385
end

386
local ExclusivelyLocal = {
387 388 389 390
  [NoLoginCode] = true,
  [InvalidRemoteCode] = true,
  [UnavailableBusCode] = true,
  [InvalidTargetCode] = true,
391 392
}

393
function Interceptor:receivereply(request)
394 395
  if not request.success then
    local except = request.results[1]
396
    if is_NO_PERMISSION(except, nil, "COMPLETED_NO") then
397
      if except.minor == InvalidCredentialCode then
398
        -- got invalid credential exception
399 400 401 402 403 404 405
        -- extract credential reset
        local reset, islegacy
        local context = self.context
        local orb = context.orb
        local types = context.types
        local service_contexts = request.reply_service_context
        local data = service_contexts[CredentialContextId]
406
        if data ~= nil then
407 408 409 410 411 412 413 414 415 416 417 418
          reset = orb:newdecoder(data):get(types.CredentialReset)
        elseif self.legacy ~= nil then
          local data = service_contexts[LegacyCredentialContextId]
          if data ~= nil then
            reset = orb:newdecoder(data):get(types.LegacyCredentialReset)
            local backup = context:exitChain()
            local target = self.LoginRegistry:getLoginEntry(reset.target)
            if backup ~= nil then context:joinChain(backup) end
            if target == nil then
              log:exception(msg.UnableToGetTargetEntity:tag{
                target = reset.target,
              })
419
              except.minor = InvalidTargetCode
420 421 422 423 424 425 426 427
              return
            end
            reset.entity = target.entity
            islegacy = true
          end
        end
        -- validate credential reset
        if reset ~= nil then
428 429 430
          local secret, errmsg = self.prvkey:decrypt(reset.challenge)
          if secret ~= nil then
            local target = reset.target
431
            local entity = reset.entity
432 433
            log:access(self, msg.GotCredentialReset:tag{
              operation = request.operation_name,
434
              entity = entity,
435
              remote = target,
436
              islegacy = islegacy,
437 438 439 440 441 442 443 444
            })
            reset.secret = secret
            -- initialize session and set credential session information
            self.profile2login:put(request.profile_data, target)
            self.outgoingSessions:put(target, {
              id = reset.session,
              secret = reset.secret,
              remote = target,
445
              entity = entity,
446
              ticket = -1,
447
              islegacy = islegacy,
448 449 450 451 452 453 454 455
            })
            request.success = nil -- reissue request to the same reference
          else
            log:exception(msg.GotCredentialResetWithBadChallenge:tag{
              operation = request.operation_name,
              remote = reset.target,
              error = errmsg,
            })
456
            except.minor = InvalidRemoteCode
457
          end
458
        else
459
          log:exception(msg.CredentialResetMissing:tag{
460 461
            operation = request.operation_name,
          })
462
          except.minor = InvalidRemoteCode
463
        end
464 465
      elseif not request.islocal and ExclusivelyLocal[except.minor] ~= nil then
        log:exception(msg.IllegalUseOfLocalMinorCodeByRemoteSite:tag{
466
          operation = request.operation_name,
467
          codeused = except.minor,
468
        })
469
        except.minor = InvalidRemoteCode
470 471 472
      end
    end
  end
473 474
end

475 476 477
function Interceptor:receiverequest(request, credential)
  local credential = credential
                  or self:unmarshalCredential(request.service_context)
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
478
  if credential ~= nil then
479
    local busid = credential.bus
480
    if busid == self.busid then
481
      local caller = self.LoginRegistry:getLoginEntry(credential.login)
482
      if caller ~= nil then
483
        local context = self.context
484
        if validateCredential(self, credential, caller, request) then
485 486
          local chain = credential.chain
          if validateChain(self, chain, caller) then
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
487
            log:access(self, msg.GotBusCall:tag{
488
              operation = request.operation_name,
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
489 490
              remote = caller.id,
              entity = caller.entity,
491
              islegacy = credential.islegacy,
492
            })
493
            context.callerChainOf[running()] = chain
494 495 496
          else
            -- invalid call chain
            log:exception(msg.GotCallWithInvalidChain:tag{
497
              operation = request.operation_name,
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
498 499
              remote = caller.id,
              entity = caller.entity,
500
              islegacy = credential.islegacy,
501
            })
502
            setNoPermSysEx(request, InvalidChainCode)
503
          end
504 505 506 507
        else
          -- invalid credential, try to reset credetial session
          local sessions = self.incomingSessions
          local newsession = sessions:get(#sessions.map+1)
508
          newsession.login = caller.id
509 510 511 512 513 514 515
          local challenge, errmsg = caller.pubkey:encrypt(newsession.secret)
          if challenge ~= nil then
            -- marshall credential reset
            log:access(self, msg.SendCredentialReset:tag{
              operation = request.operation_name,
              remote = caller.id,
              entity = caller.entity,
516
              islegacy = credential.islegacy,
517
            })
518
            local login = self.login
519
            local encoder = context.orb:newencoder()
520 521 522 523 524 525
            local idltype = context.types.CredentialReset
            local contextid = CredentialContextId
            if credential.islegacy then
              idltype = context.types.LegacyCredentialReset
              contextid = LegacyCredentialContextId
            end
526
            encoder:put({
527 528
              target = login.id,
              entity = login.entity,
529 530
              session = newsession.id,
              challenge = challenge,
531 532
            }, idltype)
            request.reply_service_context = { [contextid] = encoder:getdata() }
533
            setNoPermSysEx(request, InvalidCredentialCode)
534 535 536 537 538 539 540
          else
            log:exception(msg.UnableToEncryptSecretWithCallerKey:tag{
              operation = request.operation_name,
              remote = caller.id,
              entity = caller.entity,
              error = errmsg,
            })
541
            setNoPermSysEx(request, InvalidPublicKeyCode)
542
          end
543
        end
544
      else
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
545 546
        -- credential with invalid login
        log:exception(msg.GotCallWithInvalidLogin:tag{
547
          operation = request.operation_name,
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
548
          remote = credential.login,
549
        })
550
        setNoPermSysEx(request, InvalidLoginCode)
551 552
      end
    else
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
553 554
      -- credential for another bus
      log:exception(msg.GotCallFromAnotherBus:tag{
555
        operation = request.operation_name,
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
556
        remote = credential.login,
557
        bus = busid,
558
      })
559
      setNoPermSysEx(request, UnknownBusCode)
560 561
    end
  else
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
562
    log:access(self, msg.GotOrdinaryCall:tag{
563
      operation = request.operation_name,
564 565
    })
  end
566 567
end

568
function Interceptor:sendreply()
569
  self.context.callerChainOf[running()] = nil
570 571 572 573
end



Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
function log.custom:access(conn, ...)
  local viewer = self.viewer
  local output = viewer.output
  if self.flags.multiplex ~= nil then
    local login, busid = conn.login, conn.busid
    if login ~= nil then
      output:write(msg.CurrentLogin:tag{
        bus = busid,
        login = login.id,
        entity = login.entity,
      },"  ")
    end
  end
  for i = 1, select("#", ...) do
    local value = select(i, ...)
    if type(value) == "string"
      then output:write(value)
      else viewer:write(value)
    end
  end
end



598
local module = {
599
  Context = Context,
600
  Interceptor = Interceptor,
Renato Figueiro Maia's avatar
Renato Figueiro Maia committed
601
  setNoPermSysEx = setNoPermSysEx,
602 603
}

604
function module.initORB(configs)
605 606 607 608 609 610 611 612 613 614 615
  if configs == nil then
    configs = {}
  end
  if configs.options == nil then
    configs.options = {}
  end
  if configs.options.tcp == nil then
    configs.options.tcp = {reuseaddr=true}
  end
  if configs.flavor == nil then
    configs.flavor = "cooperative;corba.intercepted"
616 617 618
  end
  local orb = neworb(configs)
  loadidl(orb)
619
  loadoldidl(orb)
620
  return orb
621 622 623
end

return module