def test_verify(self): retrieved_msg, retrieved_dn = verify(SIGNED_MSG, self.ca_dir, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg.strip() == MSG: self.fail("The verified messge didn't match the original.") retrieved_msg2, retrieved_dn2 = verify(SIGNED_MSG2, self.ca_dir, False) if not retrieved_dn2 == CERT2_DN: print retrieved_dn2 print CERT2_DN self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg2.strip() == MSG2: print retrieved_msg2 print MSG2 self.fail("The verified messge didn't match the original.") # Try empty string try: verify('', self.ca_dir, False) except CryptoException: pass # Try rubbish try: verify('Bibbly bobbly', self.ca_dir, False) except CryptoException: pass # Try None arguments try: verify('Bibbly bobbly', None, False) except CryptoException: pass try: verify(None, 'not a path', False) except CryptoException: pass
def test_verify(self): retrieved_msg, retrieved_dn = verify(SIGNED_MSG, self.ca_dir, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg.strip() == MSG: self.fail("The verified messge didn't match the original.") retrieved_msg2, retrieved_dn2 = verify(SIGNED_MSG2, self.ca_dir, False) if not retrieved_dn2 == TEST_CERT_DN: print retrieved_dn2 print TEST_CERT_DN self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg2.strip() == MSG2: print retrieved_msg2 print MSG2 self.fail("The verified messge didn't match the original.") # Try empty string try: verify('', self.ca_dir, False) except CryptoException: pass # Try rubbish try: verify('Bibbly bobbly', self.ca_dir, False) except CryptoException: pass # Try None arguments try: verify('Bibbly bobbly', None, False) except CryptoException: pass try: verify(None, 'not a path', False) except CryptoException: pass
def test_message_tampering(self): """Test that a tampered message is not accepted as valid.""" signed_message = sign(MSG, TEST_CERT_FILE, TEST_KEY_FILE) tampered_message = signed_message.replace(MSG, "Spam") # Verifying the orignal, un-tampered message should be fine. verified_message, verified_signer = verify(signed_message, TEST_CA_DIR, False) self.assertEqual(verified_message, MSG) self.assertEqual(verified_signer, TEST_CERT_DN) # Verifying the tampered message should not be fine. self.assertRaises(CryptoException, verify, tampered_message, TEST_CA_DIR, False)
def _handle_msg(self, text): """Deal with the raw message contents appropriately. Namely: - decrypt if necessary - verify signature - Return plain-text message, signer's DN and an error/None. """ if text is None or text == '': warning = 'Empty text passed to _handle_msg.' log.warn(warning) return None, None, warning # if not text.startswith('MIME-Version: 1.0'): # raise Ssm2Exception('Not a valid message.') # encrypted - this could be nicer if 'application/pkcs7-mime' in text or 'application/x-pkcs7-mime' in text: try: text = crypto.decrypt(text, self._cert, self._key) except crypto.CryptoException as e: error = 'Failed to decrypt message: %s' % e log.error(error) return None, None, error # always signed try: message, signer = crypto.verify(text, self._capath, self._check_crls) except crypto.CryptoException as e: error = 'Failed to verify message: %s' % e log.error(error) return None, None, error if signer not in self._valid_dns: warning = 'Signer not in valid DNs list: %s' % signer log.warn(warning) return None, signer, warning else: log.info('Valid signer: %s', signer) return message, signer, None
def test_sign(self): ''' I haven't found a good way to test this yet. Each time you sign a message, the output has a random element, so you can't compare strings. ''' signed = sign(MSG, self.certpath, self.keypath) if not 'MIME-Version' in signed: self.fail("Didn't get MIME message when signing.") if not MSG in signed: self.fail('The plaintext should be included in the signed message.') # Indirect testing, using the verify_message() method retrieved_msg, retrieved_dn = verify(signed, self.ca_dir, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg == MSG: self.fail("The verified message didn't match the original.")
def test_sign(self): ''' I haven't found a good way to test this yet. Each time you sign a message, the output has a random element, so you can't compare strings. ''' signed = sign(MSG, TEST_CERT_FILE, TEST_KEY_FILE) if not 'MIME-Version' in signed: self.fail("Didn't get MIME message when signing.") if not MSG in signed: self.fail( 'The plaintext should be included in the signed message.') # Indirect testing, using the verify_message() method retrieved_msg, retrieved_dn = verify(signed, TEST_CA_DIR, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg == MSG: self.fail("The verified message didn't match the original.")
if text is None or text == '': return None, None # if not text.startswith('MIME-Version: 1.0'): # raise Ssm2Exception('Not a valid message.') # encrypted - this could be nicer if 'application/pkcs7-mime' in text or 'application/x-pkcs7-mime' in text: try: text = crypto.decrypt(text, self._cert, self._key) except crypto.CryptoException, e: log.error('Failed to decrypt message: %s', e) return None, None # always signed try: message, signer = crypto.verify(text, self._capath, self._check_crls) except crypto.CryptoException, e: log.error('Failed to verify message: %s', e) return None, None if signer not in self._valid_dns: log.warn('Signer not in valid DNs list: %s', signer) return None, signer else: log.info('Valid signer: %s', signer) return message, signer def _send_msg(self, message, msgid): ''' Send one message using stomppy. The message will be signed using
def test_verify(self): signed_msg = sign(MSG, TEST_CERT_FILE, TEST_KEY_FILE) # This is a manual 'fudge' to make MS2 appear like a # quoted-printable message when signed # Encode MSG2 so it's 'quoted-printable' quopri_msg = quopri.encodestring(MSG2) # Add Content-Type and Content-Transfer-Encoding # headers to message header_quopri_msg = ('Content-Type: text/xml; charset=utf8\n' 'Content-Transfer-Encoding: quoted-printable\n' '\n' '%s' % quopri_msg) # We can't use crypto.sign as that assumes the use of the '-text' option # which cause the message to be interpreted as plaintext p1 = Popen([ 'openssl', 'smime', '-sign', '-inkey', TEST_KEY_FILE, '-signer', TEST_CERT_FILE ], stdin=PIPE, stdout=PIPE, stderr=PIPE) signed_msg2, error = p1.communicate(header_quopri_msg) if error != '': self.fail(error) retrieved_msg, retrieved_dn = verify(signed_msg, TEST_CA_DIR, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg.strip() == MSG: self.fail("The verified messge didn't match the original.") retrieved_msg2, retrieved_dn2 = verify(signed_msg2, TEST_CA_DIR, False) if not retrieved_dn2 == TEST_CERT_DN: print retrieved_dn2 print TEST_CERT_DN self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg2.strip() == MSG2: print retrieved_msg2 print MSG2 self.fail("The verified messge didn't match the original.") # Try empty string try: verify('', TEST_CA_DIR, False) except CryptoException: pass # Try rubbish try: verify('Bibbly bobbly', TEST_CA_DIR, False) except CryptoException: pass # Try None arguments try: verify('Bibbly bobbly', None, False) except CryptoException: pass try: verify(None, 'not a path', False) except CryptoException: pass
class Ssm2(stomp.ConnectionListener): ''' Minimal SSM implementation. ''' # Schema for the dirq message queue. QSCHEMA = {'body': 'string', 'signer': 'string', 'empaid': 'string?'} REJECT_SCHEMA = { 'body': 'string', 'signer': 'string?', 'empaid': 'string?', 'error': 'string' } CONNECTION_TIMEOUT = 10 def __init__(self, hosts_and_ports, qpath, cert, key, dest=None, listen=None, capath=None, check_crls=False, use_ssl=False, username=None, password=None, enc_cert=None, verify_enc_cert=True, pidfile=None): ''' Creates an SSM2 object. If a listen value is supplied, this SSM2 will be a receiver. ''' self._conn = None self._last_msg = None self._brokers = hosts_and_ports self._cert = cert self._key = key self._enc_cert = enc_cert self._capath = capath self._check_crls = check_crls self._user = username self._pwd = password self._use_ssl = use_ssl # use pwd auth if we're supplied both user and pwd self._use_pwd = username is not None and password is not None self.connected = False self._listen = listen self._dest = dest self._valid_dns = [] self._pidfile = pidfile # create the filesystem queues for accepted and rejected messages if dest is not None and listen is None: self._outq = QueueSimple(qpath) elif listen is not None: inqpath = os.path.join(qpath, 'incoming') rejectqpath = os.path.join(qpath, 'reject') self._inq = Queue(inqpath, schema=Ssm2.QSCHEMA) self._rejectq = Queue(rejectqpath, schema=Ssm2.REJECT_SCHEMA) else: raise Ssm2Exception('SSM must be either producer or consumer.') # check that the cert and key match if not crypto.check_cert_key(self._cert, self._key): raise Ssm2Exception('Cert and key don\'t match.') # Check that the certificate has not expired. if not crypto.verify_cert_date(self._cert): raise Ssm2Exception('Certificate %s has expired.' % self._cert) # check the server certificate provided if enc_cert is not None: log.info('Messages will be encrypted using %s', enc_cert) if not os.path.isfile(self._enc_cert): raise Ssm2Exception( 'Specified certificate file does not exist: %s.' % self._enc_cert) # Check that the encyption certificate has not expired. if not crypto.verify_cert_date(enc_cert): raise Ssm2Exception( 'Encryption certificate %s has expired. Please obtain the ' 'new one from the final server receiving your messages.' % enc_cert) if verify_enc_cert: if not crypto.verify_cert_path(self._enc_cert, self._capath, self._check_crls): raise Ssm2Exception( 'Failed to verify server certificate %s against CA path %s.' % (self._enc_cert, self._capath)) # If the overall SSM log level is info, we want to only # see log entries from stomp.py at the warning level and above. if logging.getLogger("ssm.ssm2").getEffectiveLevel() == logging.INFO: logging.getLogger("stomp.py").setLevel(logging.WARNING) # If the overall SSM log level is debug, we want to only # see log entries from stomp.py at the info level and above. elif logging.getLogger( "ssm.ssm2").getEffectiveLevel() == logging.DEBUG: logging.getLogger("stomp.py").setLevel(logging.INFO) def set_dns(self, dn_list): ''' Set the list of DNs which are allowed to sign incoming messages. ''' self._valid_dns = dn_list ########################################################################## # Methods called by stomppy ########################################################################## def on_send(self, frame, unused_body=None): ''' Called by stomppy when a message is sent. unused_body is only present to have a backward compatible method signature when using stomp.py v3.1.X ''' try: # Try the stomp.py v4 way first empaid = frame.headers['empa-id'] except KeyError: # Then you are most likely using stomp.py v4. # on_send is now triggered on non message frames # (such as 'CONNECT' frames) and as such without an empa-id. empaid = 'no empa-id' except AttributeError: # Then you are likely using stomp.py v3 empaid = frame['empa-id'] log.debug('Sent message: %s', empaid) def on_message(self, headers, body): ''' Called by stomppy when a message is received. Handle the message according to its content and headers. ''' try: empaid = headers['empa-id'] if empaid == 'ping': # ignore ping message log.info('Received ping message.') return except KeyError: empaid = 'noid' log.info("Received message. ID = %s", empaid) extracted_msg, signer, err_msg = self._handle_msg(body) try: # If the message is empty or the error message is not empty # then reject the message. if extracted_msg is None or err_msg is not None: if signer is None: # crypto failed signer = 'Not available.' elif extracted_msg is not None: # If there is a signer then it was rejected for not being # in the DNs list, so we can use the extracted msg, which # allows the msg to be reloaded if needed. body = extracted_msg log.warn("Message rejected: %s", err_msg) name = self._rejectq.add({ 'body': body, 'signer': signer, 'empaid': empaid, 'error': err_msg }) log.info("Message saved to reject queue as %s", name) else: # message verified ok name = self._inq.add({ 'body': extracted_msg, 'signer': signer, 'empaid': empaid }) log.info("Message saved to incoming queue as %s", name) except (IOError, OSError) as e: log.error('Failed to read or write file: %s', e) def on_error(self, unused_headers, body): ''' Called by stomppy when an error frame is received. ''' log.warn('Error message received: %s', body) raise Ssm2Exception() def on_connected(self, unused_headers, unused_body): ''' Called by stomppy when a connection is established. Track the connection. ''' self.connected = True log.info('Connected.') def on_disconnected(self): ''' Called by stomppy when disconnected from the broker. ''' log.info('Disconnected from broker.') self.connected = False def on_receipt(self, headers, unused_body): ''' Called by stomppy when the broker acknowledges receipt of a message. ''' log.info('Broker received message: %s', headers['receipt-id']) self._last_msg = headers['receipt-id'] def on_receiver_loop_completed(self, _unused_headers, _unused_body): """ Called by stompy when the receiver loop ends. This is usually trigger as part of a disconnect. """ log.debug('on_receiver_loop_completed called.') ########################################################################## # Message handling methods ########################################################################## def _handle_msg(self, text): ''' Deal with the raw message contents appropriately: - decrypt if necessary - verify signature Return plain-text message, signer's DN and an error/None. ''' if text is None or text == '': warning = 'Empty text passed to _handle_msg.' log.warn(warning) return None, None, warning # if not text.startswith('MIME-Version: 1.0'): # raise Ssm2Exception('Not a valid message.') # encrypted - this could be nicer if 'application/pkcs7-mime' in text or 'application/x-pkcs7-mime' in text: try: text = crypto.decrypt(text, self._cert, self._key) except crypto.CryptoException, e: error = 'Failed to decrypt message: %s' % e log.error(error) return None, None, error # always signed try: message, signer = crypto.verify(text, self._capath, self._check_crls) except crypto.CryptoException, e: error = 'Failed to verify message: %s' % e log.error(error) return None, None, error
def test_verify(self): signed_msg = sign(MSG, TEST_CERT_FILE, TEST_KEY_FILE) # This is a manual 'fudge' to make MS2 appear like a # quoted-printable message when signed # Encode MSG2 so it's 'quoted-printable', after encoding it to ensure # it's a bytes object for Python 3. Latter is a no-op in Python 2. quopri_msg = quopri.encodestring(MSG2.encode()) # In Python 3, encodestring() returns bytes so decode to a string while # Python 2 compatability is still required. if not isinstance(quopri_msg, str): quopri_msg = quopri_msg.decode() # Add Content-Type and Content-Transfer-Encoding # headers to message header_quopri_msg = ('Content-Type: text/xml; charset=utf8\n' 'Content-Transfer-Encoding: quoted-printable\n' '\n' '%s' % quopri_msg) # We can't use crypto.sign as that assumes the use of the '-text' option # which cause the message to be interpreted as plaintext p1 = Popen([ 'openssl', 'smime', '-sign', '-inkey', TEST_KEY_FILE, '-signer', TEST_CERT_FILE ], stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True) signed_msg2, error = p1.communicate(header_quopri_msg) if error != '': self.fail(error) retrieved_msg, retrieved_dn = verify(signed_msg, TEST_CA_DIR, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg.strip() == MSG: self.fail("The verified messge didn't match the original.") retrieved_msg2, retrieved_dn2 = verify(signed_msg2, TEST_CA_DIR, False) if not retrieved_dn2 == TEST_CERT_DN: print(retrieved_dn2) print(TEST_CERT_DN) self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg2.strip() == MSG2: print(retrieved_msg2) print(MSG2) self.fail("The verified messge didn't match the original.") # Try empty string try: verify('', TEST_CA_DIR, False) except CryptoException: pass # Try rubbish try: verify('Bibbly bobbly', TEST_CA_DIR, False) except CryptoException: pass # Try None arguments try: verify('Bibbly bobbly', None, False) except CryptoException: pass try: verify(None, 'not a path', False) except CryptoException: pass
def test_verify(self): signed_msg = sign(MSG, TEST_CERT_FILE, TEST_KEY_FILE) # This is a manual 'fudge' to make MS2 appear like a # quoted-printable message when signed # Encode MSG2 so it's 'quoted-printable' quopri_msg = quopri.encodestring(MSG2) # Add Content-Type and Content-Transfer-Encoding # headers to message header_quopri_msg = ('Content-Type: text/xml; charset=utf8\n' 'Content-Transfer-Encoding: quoted-printable\n' '\n' '%s' % quopri_msg) # We can't use crypto.sign as that assumes the use of the '-text' option # which cause the message to be interpreted as plaintext p1 = Popen(['openssl', 'smime', '-sign', '-inkey', TEST_KEY_FILE, '-signer', TEST_CERT_FILE], stdin=PIPE, stdout=PIPE, stderr=PIPE) signed_msg2, error = p1.communicate(header_quopri_msg) if error != '': self.fail(error) retrieved_msg, retrieved_dn = verify(signed_msg, TEST_CA_DIR, False) if not retrieved_dn == TEST_CERT_DN: self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg.strip() == MSG: self.fail("The verified messge didn't match the original.") retrieved_msg2, retrieved_dn2 = verify(signed_msg2, TEST_CA_DIR, False) if not retrieved_dn2 == TEST_CERT_DN: print retrieved_dn2 print TEST_CERT_DN self.fail("The DN of the verified message didn't match the cert.") if not retrieved_msg2.strip() == MSG2: print retrieved_msg2 print MSG2 self.fail("The verified messge didn't match the original.") # Try empty string try: verify('', TEST_CA_DIR, False) except CryptoException: pass # Try rubbish try: verify('Bibbly bobbly', TEST_CA_DIR, False) except CryptoException: pass # Try None arguments try: verify('Bibbly bobbly', None, False) except CryptoException: pass try: verify(None, 'not a path', False) except CryptoException: pass
if text is None or text == '': return None, None # if not text.startswith('MIME-Version: 1.0'): # raise Ssm2Exception('Not a valid message.') # encrypted - this could be nicer if 'application/pkcs7-mime' in text or 'application/x-pkcs7-mime' in text: try: text = crypto.decrypt(text, self._cert, self._key) except crypto.CryptoException, e: log.error('Failed to decrypt message: %s' % e) return None, None # always signed try: message, signer = crypto.verify(text, self._capath, self._check_crls) except crypto.CryptoException, e: log.error('Failed to verify message: %s' % e) return None, None if signer not in self._valid_dns: log.error('Message signer not in the valid DNs list: %s' % signer) return None, signer else: log.info('Valid signer: %s' % signer) return message, signer def _send_msg(self, message, msgid): ''' Send one message using stomppy. The message will be signed using