From f6ce9f2c1eb7a156a32134546af54836f8eb9eb0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 20 Nov 2015 17:05:36 -0500 Subject: [PATCH] Add infrastructure for TLS certificate handling When connecting to peer devices that use TLS, provide a mechanism of tracking peer fingerprint and handling missing or mismatch of fingerprint. --- .../confluent/config/attributes.py | 15 ++++++++ confluent_server/confluent/exceptions.py | 24 +++++++++++- confluent_server/confluent/httpapi.py | 6 +++ .../plugins/hardwaremanagement/ipmi.py | 4 ++ confluent_server/confluent/sockapi.py | 9 +++++ confluent_server/confluent/util.py | 38 +++++++++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) 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..c7711e66 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,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + class ConfluentException(Exception): - pass + apierrorcode = 500 + apierrorstr = 'Unexpected Error' + + def get_error_body(self): + return self.apierrorstr class NotFoundException(ConfluentException): @@ -42,6 +49,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 +66,20 @@ 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, fingerprint, attribname): + super(PubkeyInvalid, self).__init__(self, text) + self.fingerprint = fingerprint + self.errorbody = json.dumps({attribname: fingerprint}) + + 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..5ca59615 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,37 @@ 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', + 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', fingerprint, self.fieldname)