Skip to content

Commit 11888d0

Browse files
authored
Merge pull request #117 from DevoInc/feature/sender_configuration_improvements
# [3.6.0] - 2022-05-17 ## Added * Sender: certificate files can now be verified with `verify_config=True` or `"verify_config": true` from the config file. * Internal support for HTTP unsecure API REST endpoint. ## Fixed * Sender: bad error management when `socker.shutdown` is called and the connection was not established. * test `test_get_common_names` not running. * Some environment vars for testing were wrong in the sample file. * `pem` module added to depedencies
2 parents f22aa6b + 93d47ba commit 11888d0

File tree

10 files changed

+546
-22
lines changed

10 files changed

+546
-22
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [3.6.0] - 2022-05-17
8+
### Added
9+
* Sender: certificate files can now be verified with `verify_config=True` or `"verify_config": true` from the config file.
10+
* Internal support for HTTP unsecure API REST endpoint.
11+
12+
### Fixed
13+
* Sender: bad error management when `socker.shutdown` is called and the connection was not established.
14+
* test `test_get_common_names` not running.
15+
* Some environment vars for testing were wrong in the sample file.
16+
* `pem` module added to depedencies
17+
18+
719
## [3.5.0] - 2022-01-20
820
### Added
921
* Double quotes on lookups can be escaped by adding `"escape_quotes": true` to the config file.

devo/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
__description__ = 'Devo Python Library.'
22
__url__ = 'http://www.devo.com'
3-
__version__ = "3.5.0"
3+
__version__ = "3.6.0"
44
__author__ = 'Devo'
55
__author_email__ = 'support@devo.com'
66
__license__ = 'MIT'

devo/api/client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Main class for pull data from Devo API (Client)."""
33
import hmac
44
import hashlib
5+
import os
56
import time
67
import json
78
import requests
@@ -177,6 +178,9 @@ def __init__(self, address=None, auth=None, config=None,
177178
self.timeout = int(timeout) if timeout else 30
178179
self.verify = verify if verify is not None else True
179180

181+
# For internal testing purposes, Devo will never expose a REST service in an unsecure manner
182+
self.unsecure_http = True if os.getenv("UNSECURE_HTTP", "False").upper() == "TRUE" else False
183+
180184
@staticmethod
181185
def _from_dict(config):
182186
"""
@@ -349,8 +353,9 @@ def _make_request(self, payload):
349353
tries = 0
350354
while tries < self.retries:
351355
try:
352-
response = requests.post("https://{}"
353-
.format("/".join(self.address)),
356+
response = requests.post("{}://{}".format(
357+
"http" if self.unsecure_http else "https",
358+
"/".join(self.address)),
354359
data=payload,
355360
headers=self._get_headers(payload),
356361
verify=self.verify,

devo/sender/data.py

Lines changed: 194 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
# -*- coding: utf-8 -*-
22
""" File to group all the classes and functions related to the connection
33
and sending of data to Devo """
4+
import errno
45
import logging
56
import socket
67
import ssl
78
import sys
89
import time
910
import zlib
10-
from devo.common import get_stream_handler, get_log, Configuration
11-
from .transformsyslog import FORMAT_MY, FORMAT_MY_BYTES, \
12-
FACILITY_USER, SEVERITY_INFO, COMPOSE, \
13-
COMPOSE_BYTES, priority_map
11+
from pathlib import Path
12+
from _socket import SHUT_RDWR
1413

14+
import pem
15+
from devo.common import Configuration, get_log, get_stream_handler
16+
from OpenSSL import SSL, crypto
17+
18+
from .transformsyslog import (COMPOSE, COMPOSE_BYTES, FACILITY_USER, FORMAT_MY,
19+
FORMAT_MY_BYTES, SEVERITY_INFO, priority_map)
1520

1621
PYPY = hasattr(sys, 'pypy_version_info')
1722

@@ -38,18 +43,21 @@ class SenderConfigSSL:
3843
:param pkcs: (dict) (path: pfx src file, password: of cert)
3944
:param sec_level: (int) default None. If certs are too weak you can change
4045
this param to work with it
46+
:param verify_mode: (bool) Verification for the configuration
4147
4248
>>>sender_config = SenderConfigSSL(address=(SERVER, int(PORT)), key=KEY,
4349
... cert=CERT, chain=CHAIN, sec_level=None,
44-
check_hostname=True, verify_mode=None)
50+
... check_hostname=True,
51+
... verify_mode=None,
52+
... verify_config=False)
4553
4654
See Also:
4755
Sender
4856
4957
"""
5058
def __init__(self, address=None, key=None, cert=None, chain=None,
5159
pkcs=None, sec_level=None, check_hostname=True,
52-
verify_mode=None):
60+
verify_mode=None, verify_config=False):
5361
if not isinstance(address, tuple):
5462
raise DevoSenderException(
5563
"Devo-SenderConfigSSL| address must be a tuple "
@@ -63,12 +71,177 @@ def __init__(self, address=None, key=None, cert=None, chain=None,
6371
self.hostname = socket.gethostname()
6472
self.sec_level = sec_level
6573
self.check_hostname = check_hostname
74+
self.verify_config = verify_config
6675
self.verify_mode = verify_mode
6776
except Exception as error:
6877
raise DevoSenderException(
6978
"Devo-SenderConfigSSL|Can't create SSL config: "
7079
"%s" % str(error))
7180

81+
if self.verify_config:
82+
self.check_config_files_path()
83+
self.check_config_certificate_key()
84+
self.check_config_certificate_chain()
85+
self.check_config_certificate_address()
86+
87+
def check_config_files_path(self):
88+
"""
89+
Check if the certificate files
90+
in the configurations are path correct.
91+
92+
:return: Boolean true or raises an exception
93+
"""
94+
certificates = [self.key, self.chain, self.cert]
95+
for file in certificates:
96+
try:
97+
if not Path(file).is_file():
98+
raise DevoSenderException(
99+
"Error in the configuration, "
100+
+ file +
101+
" is not a file or the path does not exist")
102+
except IOError as message:
103+
if message.errno == errno.EACCES:
104+
raise DevoSenderException(
105+
"Error in the configuration "
106+
+ file + " can't be read" +
107+
"\noriginal error: " +
108+
str(message))
109+
else:
110+
raise DevoSenderException(
111+
"Error in the configuration, "
112+
+ file + " problem related to: " + str(message))
113+
114+
return True
115+
116+
def check_config_certificate_key(self):
117+
"""
118+
Check if both the certificate and the key
119+
in the configuration are compatible with each other.
120+
121+
:return: Boolean true or raises an exception
122+
"""
123+
124+
with open(self.cert, "rb") as certificate_file, \
125+
open(self.key, "rb") as key_file:
126+
127+
certificate_raw = certificate_file.read()
128+
key_raw = key_file.read()
129+
certificate_obj = crypto.load_certificate(
130+
crypto.FILETYPE_PEM, certificate_raw)
131+
private_key_obj = crypto.load_privatekey(
132+
crypto.FILETYPE_PEM, key_raw)
133+
context = SSL.Context(SSL.TLS_CLIENT_METHOD)
134+
context.use_privatekey(private_key_obj)
135+
context.use_certificate(certificate_obj)
136+
try:
137+
context.check_privatekey()
138+
except SSL.Error as message:
139+
raise DevoSenderException(
140+
"Error in the configuration, the key: " + self.key +
141+
" is not compatible with the cert: " + self.cert +
142+
"\noriginal error: " + str(message))
143+
return True
144+
145+
def check_config_certificate_chain(self):
146+
"""
147+
Check if both the certificate and the chain
148+
in the configuration are compatible with each other.
149+
150+
:return: Boolean true or raises an exception
151+
"""
152+
with open(self.cert, "rb") as certificate_file, \
153+
open(self.chain, "rb") as chain_file:
154+
155+
certificate_raw = certificate_file.read()
156+
chain_raw = chain_file.read()
157+
certificate_obj = crypto.load_certificate(
158+
crypto.FILETYPE_PEM, certificate_raw)
159+
certificates_chain = crypto.X509Store()
160+
for certificate in pem.parse(chain_raw):
161+
certificates_chain.add_cert(
162+
crypto.load_certificate(
163+
crypto.FILETYPE_PEM, str(certificate)))
164+
store_ctx = crypto.X509StoreContext(
165+
certificates_chain, certificate_obj)
166+
try:
167+
store_ctx.verify_certificate()
168+
except crypto.X509StoreContextError as message:
169+
raise DevoSenderException(
170+
"Error in config, the chain: " + self.chain +
171+
" is not compatible with the certificate: " + self.cert +
172+
"\noriginal error: " + str(message))
173+
return True
174+
175+
def check_config_certificate_address(self):
176+
"""
177+
Check if the certificate is compatible with the
178+
address, also check is the address and port are
179+
valid.
180+
181+
:return: Boolean true or raises an exception
182+
"""
183+
sock = socket.socket()
184+
context = SSL.Context(SSL.TLS_CLIENT_METHOD)
185+
sock.settimeout(10)
186+
connection = SSL.Connection(context, sock)
187+
try:
188+
connection.connect(self.address)
189+
except socket.timeout as message:
190+
raise DevoSenderException(
191+
"Possible error in config, a timeout could be related " +
192+
"to an incorrect address/port: " + str(self.address) +
193+
"\noriginal error: " + str(message))
194+
except ConnectionRefusedError as message:
195+
raise DevoSenderException(
196+
"Error in config, incorrect address/port: "
197+
+ str(self.address) +
198+
"\noriginal error: " + str(message))
199+
sock.setblocking(True)
200+
connection.do_handshake()
201+
server_chain = connection.get_peer_cert_chain()
202+
connection.close()
203+
204+
with open(self.chain, "rb") as chain_file:
205+
chain = chain_file.read()
206+
chain_certs = []
207+
for _ca in pem.parse(chain):
208+
chain_certs.append(crypto.load_certificate
209+
(crypto.FILETYPE_PEM, str(_ca)))
210+
211+
server_common_names = \
212+
self.get_common_names(server_chain, "get_subject")
213+
client_common_names = \
214+
self.get_common_names(chain_certs, "get_issuer")
215+
216+
if server_common_names & client_common_names:
217+
return True
218+
219+
raise DevoSenderException(
220+
"Error in config, the certificate in the address: "
221+
+ self.address[0] +
222+
" is not compatible with: " +
223+
self.chain)
224+
225+
@staticmethod
226+
def get_common_names(cert_chain, components_type):
227+
result = set()
228+
for temp_cert in cert_chain:
229+
for key, value in getattr(temp_cert, components_type)()\
230+
.get_components():
231+
if key.decode("utf-8") == "CN":
232+
result.add(value)
233+
return result
234+
235+
@staticmethod
236+
def fake_get_peer_cert_chain(chain):
237+
with open(chain, "rb") as chain_file:
238+
chain_certs = []
239+
for _ca in pem.parse(chain_file.read()):
240+
chain_certs.append(
241+
crypto.load_certificate(
242+
crypto.FILETYPE_PEM, str(_ca)))
243+
return chain_certs
244+
72245

73246
class SenderConfigTCP:
74247
"""
@@ -121,6 +294,14 @@ def __init__(self, config=None, con_type=None,
121294
if config is None:
122295
raise DevoSenderException("Problems with args passed to Sender")
123296

297+
self.socket = None
298+
self.reconnection = 0
299+
self.debug = debug
300+
self.socket_timeout = timeout
301+
self.socket_max_connection = 3600 * 1000
302+
self.buffer = SenderBuffer()
303+
self.logging = {}
304+
124305
self.timestart = time.time()
125306
if isinstance(config, (dict, Configuration)):
126307
timeout = config.get("timeout", timeout)
@@ -132,6 +313,7 @@ def __init__(self, config=None, con_type=None,
132313
get_log(handler=get_stream_handler(
133314
msg_format='%(asctime)s|%(levelname)s|Devo-Sender|%(message)s'))
134315

316+
135317
self._sender_config = config
136318

137319
if self._sender_config.sec_level is not None:
@@ -141,14 +323,6 @@ def __init__(self, config=None, con_type=None,
141323
_sender_config.
142324
sec_level))
143325

144-
self.socket = None
145-
self.reconnection = 0
146-
self.debug = debug
147-
self.socket_timeout = timeout
148-
self.socket_max_connection = 3600 * 1000
149-
self.buffer = SenderBuffer()
150-
self.logging = {}
151-
152326
if isinstance(config, SenderConfigSSL):
153327
self.__connect_ssl()
154328

@@ -344,7 +518,10 @@ def close(self):
344518
Forces socket closure
345519
"""
346520
if self.socket is not None:
347-
self.socket.shutdown(2)
521+
try:
522+
self.socket.shutdown(SHUT_RDWR)
523+
except: # Try else continue
524+
pass
348525
self.socket.close()
349526
self.socket = None
350527

@@ -595,6 +772,8 @@ def _from_dict(config=None, con_type=None):
595772
pkcs=config.get("pkcs", None),
596773
sec_level=config.get("sec_level", None),
597774
verify_mode=config.get("verify_mode", None),
775+
verify_config=config.get(
776+
"verify_config", False),
598777
check_hostname=config.get("check_hostname",
599778
True))
600779

docs/sender/data.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Class SenderConfigSSL accept various types of certificates, you has:
2727
+ pkcs **(_dict_)**: (path: pfx src file, password: of cert)
2828
+ sec_level **(_int_)**: Security level to openssl launch
2929
+ check_hostname **(_bool_)**: Verify cert hostname. Default: True
30+
+ verify_config **(_bool_)**: Verify the configuration file. Default: False
3031
+ verify_mode **(_int_)**: Verify mode for SSL Socket. Default: SSL default.You need use int "0" (CERT_NONE), "1" (CERT_OPTIONAL) or "2" (CERT_REQUIRED)
3132

3233
You can use the collector in some ways:
@@ -253,3 +254,35 @@ config = {"address": "devo.collertor", "port": 443,
253254
con = Sender.for_logging(config=config, tag="my.app.test.logging")
254255
logger = get_log(name="devo_logger", handler=con)
255256
```
257+
## Enabling verification for SenderConfigSSL configuration file
258+
259+
To help troubleshoot any problems with the configuration file the variables:
260+
261+
+ address **(_tuple_)**: (Server address, port)
262+
+ key **(_str_)**: key src file
263+
+ cert **(_str_)**: cert src file
264+
+ chain **(_str_)**: chain src file
265+
266+
Can be verified by adding `"verify_config": true` to the configuration file, in case any of the variables is invalid or incompatible with each other a `DevoSenderException` will be raised indicating the variable that’s causing the trouble, below an example of the file and an exception:
267+
268+
```json
269+
{
270+
"sender": {
271+
"address":"devo-relay",
272+
"port": 443,
273+
"key": "/devo/certs/key.key",
274+
"cert": "/devo/certs/cert.crt",
275+
"chain": "/devo/certs/chain.crt",
276+
"verify_config": true
277+
},
278+
"lookup": {
279+
"name": "Test lookup",
280+
"file": "/lookups/lookup.csv",
281+
"lkey": "KEY"
282+
}
283+
}
284+
```
285+
286+
```
287+
devo.sender.data.DevoSenderException: Error in the configuration, the key: /devo/certs/key.key is not compatible with the cert: /devo/certs/cert.crt
288+
```

0 commit comments

Comments
 (0)