diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py
index 1252abff..3cbd5b8c 100644
--- a/confluent_server/confluent/config/attributes.py
+++ b/confluent_server/confluent/config/attributes.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+# 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.
@@ -263,4 +264,18 @@ node = {
'IPMI are given their own settings with distinct '
'behaviors'),
},
+ 'pubkeys.addpolicy': {
+ 'description': ('Policy to use when encountering unknown public '
+ 'keys. Choices are "automatic" to accept and '
+ 'store new key if no key known and "manual" '
+ 'to always reject a new key, even if no key known'
+ 'Note that if the trusted CA verifies the certificate,'
+ ' that is accepted ignoring this policy. Default '
+ 'policy is "automatic"'),
+ 'valid_values': ('automatic', 'manual'),
+ },
+ 'pubkeys.tls_hardwaremanager': {
+ 'description': ('Fingerprint of the TLS certificate recognized as'
+ 'belonging to the hardware manager of the server'),
+ },
}
diff --git a/confluent_server/confluent/exceptions.py b/confluent_server/confluent/exceptions.py
index 15d26dbe..32d573ef 100644
--- a/confluent_server/confluent/exceptions.py
+++ b/confluent_server/confluent/exceptions.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+# 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.
@@ -14,9 +15,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import base64
+import json
+
class ConfluentException(Exception):
- pass
+ apierrorcode = 500
+ apierrorstr = 'Unexpected Error'
+
+ def get_error_body(self):
+ return self.apierrorstr
class NotFoundException(ConfluentException):
@@ -42,6 +50,7 @@ class TargetEndpointBadCredentials(ConfluentException):
# failed
pass
+
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
@@ -58,6 +67,23 @@ class NotImplementedException(ConfluentException):
# the requested task. http code 501
pass
+
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
pass
+
+
+class PubkeyInvalid(ConfluentException):
+ apierrorcode = 502
+ apierrorstr = '502 - Invalid certificate or key on target'
+
+ def __init__(self, text, certificate, fingerprint, attribname):
+ super(PubkeyInvalid, self).__init__(self, text)
+ self.fingerprint = fingerprint
+ bodydata = {'fingerprint': fingerprint,
+ 'fingerprintfield': attribname,
+ 'certificate': base64.b64encode(certificate)}
+ self.errorbody = json.dumps(bodydata)
+
+ def get_error_body(self):
+ return self.errorbody
diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py
index ec529da7..f294c7c8 100644
--- a/confluent_server/confluent/httpapi.py
+++ b/confluent_server/confluent/httpapi.py
@@ -439,6 +439,12 @@ def resourcehandler_backend(env, start_response):
except exc.NotImplementedException:
start_response('501 Not Implemented', headers)
yield '501 Not Implemented'
+ except exc.ConfluentException as e:
+ if e.apierrorcode == 500:
+ # raise generics to trigger the tracelog
+ raise
+ start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr))
+ yield e.get_error_body()
def _assemble_html(responses, resource, querydict, url, extension):
yield '
' \
diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py
index b6407993..8144d8c2 100644
--- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py
+++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py
@@ -17,6 +17,7 @@ import atexit
import confluent.exceptions as exc
import confluent.interface.console as conapi
import confluent.messages as msg
+import confluent.util as util
import eventlet
import eventlet.event
import eventlet.green.threading as threading
@@ -94,6 +95,9 @@ class IpmiCommandWrapper(ipmicommand.Command):
'secret.hardwaremanagementpassword', 'secret.ipmikg',
'hardwaremanagement.manager'), self._attribschanged)
super(self.__class__, self).__init__(**kwargs)
+ self.register_key_handler(
+ util.TLSCertVerifier(
+ cfm, node, 'pubkeys.tls_hardwaremanager').verify_cert)
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
try:
diff --git a/confluent_server/confluent/sockapi.py b/confluent_server/confluent/sockapi.py
index d075070f..27caab99 100644
--- a/confluent_server/confluent/sockapi.py
+++ b/confluent_server/confluent/sockapi.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+# 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.
@@ -145,6 +146,14 @@ def sessionhdl(connection, authname, skipauth=False):
send_data(connection, {'errorcode': 500,
'error': 'Locked Credential Store'})
send_data(connection, {'_requestdone': 1})
+ except exc.ConfluentException as e:
+ if e.apierrorcode == 500:
+ tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
+ event=log.Events.stacktrace)
+ send_data(connection, {'errorcode': e.apierrorcode,
+ 'error': e.apierrorstr,
+ 'detail': e.get_error_body()})
+ send_data(connection, {'_requestdone': 1})
except SystemExit:
sys.exit(0)
except:
diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py
index ce815634..99d140c1 100644
--- a/confluent_server/confluent/util.py
+++ b/confluent_server/confluent/util.py
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+# 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.
@@ -16,6 +17,9 @@
# Various utility functions that do not neatly fit into one category or another
import base64
+import confluent.exceptions as cexc
+import confluent.log as log
+import hashlib
import os
import struct
@@ -56,3 +60,39 @@ def monotonic_time():
"""
# for now, just support POSIX systems
return os.times()[4]
+
+class TLSCertVerifier(object):
+ def __init__(self, configmanager, node, fieldname):
+ self.cfm = configmanager
+ self.node = node
+ self.fieldname = fieldname
+
+ def verify_cert(self, certificate):
+ fingerprint = 'sha512$' + hashlib.sha512(certificate).hexdigest()
+ storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
+ )
+ if self.fieldname not in storedprint[self.node]: # no stored value, check
+ # policy for next action
+ newpolicy = self.cfm.get_node_attributes(self.node,
+ ('pubkeys.addpolicy',))
+ if ('pubkeys.addpolicy' in newpolicy[self.node] and
+ 'value' in newpolicy[self.node]['pubkeys.addpolicy'] and
+ newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
+ # manual policy means always raise unless a match is set
+ # manually
+ raise cexc.PubkeyInvalid('New certificate detected',
+ certificate, fingerprint,
+ self.fieldname)
+ # since the policy is not manual, go ahead and add new key
+ # after logging to audit log
+ auditlog = log.Logger('audit')
+ auditlog.log({'node': self.node, 'event': 'certautoadd',
+ 'fingerprint': fingerprint})
+ self.cfm.set_node_attributes(
+ {self.node: {self.fieldname: fingerprint}})
+ return True
+ elif storedprint[self.node][self.fieldname]['value'] == fingerprint:
+ return True
+ raise cexc.PubKeyInvalid(
+ 'Mismatched certificate detected', certificate, fingerprint,
+ self.fieldname)