# Copyright 2015 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This represents the server side of a session object Split into a separate file to avoid overly manipulating the as-yet client-centered session object """ import collections import hashlib import hmac import os import socket import struct import uuid import aiohmi.ipmi.private.constants as constants import aiohmi.ipmi.private.session as ipmisession class ServerSession(ipmisession.Session): def __new__(cls, authdata, kg, clientaddr, netsocket, request, uuid, bmc): # Need to do default new type behavior. The normal session # takes measures to assure the caller shares even when they # didn't try. We don't have that operational mode to contend # with in the server case (one file descriptor per bmc) return object.__new__(cls) def create_open_session_response(self, request): clienttag = request[0] # role = request[1] self.clientsessionid = request[4:8] # TODO(jbjohnso): intelligently handle integrity/auth/conf # for now, forcibly do cipher suite 3 self.managedsessionid = os.urandom(4) # table 13-17, 1 for now (hmac-sha1), 3 should also be supported # table 13-18, integrity, 1 for now is hmac-sha1-96, 4 is sha256 # confidentiality: 1 is aes-cbc-128, the only one self.privlevel = 4 response = (bytearray([clienttag, 0, self.privlevel, 0]) + self.clientsessionid + self.managedsessionid + bytearray([ 0, 0, 0, 8, 1, 0, 0, 0, # auth 1, 0, 0, 8, 1, 0, 0, 0, # integrity 2, 0, 0, 8, 1, 0, 0, 0, # privacy ])) return response def __init__(self, authdata, kg, clientaddr, netsocket, request, uuid, bmc): # begin conversation per RMCP+ open session request self.uuid = uuid self.currhashlib = hashlib.sha1 self.currhashlen = 12 self.rqaddr = constants.IPMI_BMC_ADDRESS self.authdata = authdata self.servermode = True self.ipmiversion = 2.0 self.sequencenumber = 0 self.sessionid = 0 self.bmc = bmc self.lastpayload = None self.rqlun = None # This will be provided by the client self.broken = False self.authtype = 6 self.integrityalgo = 0 self.confalgo = 0 self.kg = kg self.socket = netsocket self.sockaddr = clientaddr self.pendingpayloads = collections.deque([]) self.pktqueue = collections.deque([]) if clientaddr not in ipmisession.Session.bmc_handlers: ipmisession.Session.bmc_handlers[clientaddr] = {bmc.port: self} else: ipmisession.Session.bmc_handlers[clientaddr][bmc.port] = self response = self.create_open_session_response(bytearray(request)) self.send_payload(response, constants.payload_types['rmcpplusopenresponse'], retry=False) def _got_rmcp_openrequest(self, data): response = self.create_open_session_response( struct.pack('B' * len(data), *data)) self.send_payload(response, constants.payload_types['rmcpplusopenresponse'], retry=False) def _got_rakp1(self, data): clienttag = data[0] self.Rm = data[8:24] self.rolem = data[24] self.maxpriv = self.rolem & 0b111 namepresent = data[27] if namepresent == 0: # ignore null username for now return self.username = bytes(data[28:]) password = self.authdata.get(self.username.decode('utf-8')) if password is None: # don't think about invalid usernames for now return uuidbytes = self.uuid.bytes self.uuiddata = uuidbytes self.Rc = os.urandom(16) hmacdata = (self.clientsessionid + self.managedsessionid + self.Rm + self.Rc + uuidbytes + bytearray([self.rolem, len(self.username)])) hmacdata += self.username self.kuid = password.encode('utf-8') if self.kg is None: self.kg = self.kuid authcode = hmac.new( self.kuid, bytes(hmacdata), hashlib.sha1).digest() # regretably, ipmi mandates the server send out an hmac first # akin to a leak of /etc/shadow, not too worrisome if the secret # is complex, but terrible for most likely passwords selected by # a human newmessage = (bytearray([clienttag, 0, 0, 0]) + self.clientsessionid + self.Rc + uuidbytes + authcode) self.send_payload(newmessage, constants.payload_types['rakp2'], retry=False) def _got_rakp2(self, data): # stub, server should not think about rakp2 pass def _got_rakp3(self, data): # for now drop rakp3 with bad authcode # respond correctly a TODO(jjohnson2), since Kg being used # yet incorrect is a scenario why rakp3 could be bad # even if rakp2 was good RmRc = self.Rm + self.Rc self.sik = hmac.new(self.kg, bytes(RmRc) + struct.pack("2B", self.rolem, len(self.username)) + self.username, hashlib.sha1).digest() self.k1 = hmac.new(self.sik, b'\x01' * 20, hashlib.sha1).digest() self.k2 = hmac.new(self.sik, b'\x02' * 20, hashlib.sha1).digest() self.aeskey = self.k2[0:16] hmacdata = (self.Rc + self.clientsessionid + struct.pack("2B", self.rolem, len(self.username)) + self.username) expectedauthcode = hmac.new(self.kuid, bytes(hmacdata), hashlib.sha1 ).digest() authcode = struct.pack("%dB" % len(data[8:]), *data[8:]) if expectedauthcode != authcode: # TODO(jjohnson2): RMCP error back at invalid rakp3 return clienttag = data[0] if data[1] != 0: # client did not like our response, so ignore the rakp3 return self.localsid = struct.unpack(' 1: if pendingpriv > self.maxpriv: returncode = 0x81 else: self.clientpriv = request['data'][0] self._send_ipmi_net_payload(code=returncode, data=[self.clientpriv]) elif request['netfn'] == 6 and request['command'] == 0x3c: self.send_ipmi_response() self.close_server_session() else: self.bmc.handle_raw_request(request, self) def close_server_session(self): pass def _send_rakp4(self, tagvalue, statuscode): payload = bytearray( [tagvalue, statuscode, 0, 0]) + self.clientsessionid hmacdata = self.Rm + self.managedsessionid + self.uuiddata hmacdata = struct.pack('%dB' % len(hmacdata), *hmacdata) authdata = hmac.new(self.sik, hmacdata, hashlib.sha1).digest()[:12] payload += authdata self.send_payload(payload, constants.payload_types['rakp4'], retry=False) self.confalgo = 'aes' self.integrityalgo = 'sha1' self.sequencenumber = 1 self.sessionid = struct.unpack( '> 2 mylun = netfnlun & 0b11 if netfn == 6: # application request if data[19] == 0x38: # cmd = get channel auth capabilities verchannel, level = struct.unpack('2B', bytes(data[20:22])) version = verchannel & 0b10000000 if version != 0b10000000: return channel = verchannel & 0b1111 if channel != 0xe: return (clientaddr, clientlun) = struct.unpack( 'BB', bytes(data[17:19])) clientseq = clientlun >> 2 clientlun &= 0b11 # Lun is only the least significant bits level &= 0b1111 self.send_auth_cap(myaddr, mylun, clientaddr, clientlun, clientseq, sockaddr) elif data[19] == 0x54: clientaddr, clientlun = data[17:19] clientseq = clientlun >> 2 clientlun &= 0b11 self.send_cipher_suites(myaddr, mylun, clientaddr, clientlun, clientseq, data, sockaddr) def set_kg(self, kg): """Sets the Kg for the BMC to use In RAKP, Kg is a BMC-specific integrity key that can be set. If not set, Kuid is used for the integrity key """ try: self.kg = kg.encode('utf-8') except AttributeError: self.kg = kg def send_device_id(self, session): response = [self.deviceid, self.revision, self.firmwaremajor, self.firmwareminor, self.ipmiversion, self.additionaldevices] response += struct.unpack('4B', struct.pack('