Actually validate door controller SSL certificates

This commit is contained in:
Adam Goldsmith 2020-06-01 02:02:26 -04:00
parent 4d38ac5840
commit 0528686903
5 changed files with 52 additions and 9 deletions

View File

@ -33,3 +33,15 @@ Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQ
## Systemd ## Systemd
There are systemd units in the [`systemd`](./systemd/) folder, which can be used to run the various scripts regularly. There are systemd units in the [`systemd`](./systemd/) folder, which can be used to run the various scripts regularly.
## SSL Certificates
The HID Evo Solo door controllers we use have a self-signed certificate, which is included in this repo as [`hidglobal.com.pem`](./hidglobal.com.pem).
If you need to use a different certificate, you can either download it with your browser or the following command (replacing `SERVER` by the address of the door):
```sh
openssl s_client -connect SERVER:443 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > example.pem
```
Then set `doorControllerCA_BUNDLE` in `config.yaml` to the path to the created pem file. If your doors have different certificates, (which ours annoyingly don't), you will need to concatenate the certificates together into a single file.

View File

@ -6,6 +6,8 @@ doorControllers:
Wood Shop Rear: {ip: 172.18.51.15, access: Wood Shop} Wood Shop Rear: {ip: 172.18.51.15, access: Wood Shop}
Storage Closet: {ip: 172.18.51.16, access: Storage Closet} Storage Closet: {ip: 172.18.51.16, access: Storage Closet}
doorControllerCA_BUNDLE: "hidglobal.com.pem"
# {member type: door schedule} # {member type: door schedule}
memberLevels: memberLevels:
CMS Staff: 7x24 CMS Staff: 7x24

24
hidglobal.com.pem Normal file
View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID9TCCAt2gAwIBAgIJAPBotjnyfGu6MA0GCSqGSIb3DQEBCwUAMIGQMQswCQYD
VQQGEwJVUzERMA8GA1UECAwIQ29sb3JhZG8xFDASBgNVBAcMC1dlc3RtaW5zdGVy
MQwwCgYDVQQKDANISUQxDDAKBgNVBAsMA05BUzEWMBQGA1UEAwwNaGlkZ2xvYmFs
LmNvbTEkMCIGCSqGSIb3DQEJARYVc3VwcG9ydEBoaWRnbG9iYWwuY29tMB4XDTE5
MDcyMjEwMTQxMFoXDTI5MDcxOTEwMTQxMFowgZAxCzAJBgNVBAYTAlVTMREwDwYD
VQQIDAhDb2xvcmFkbzEUMBIGA1UEBwwLV2VzdG1pbnN0ZXIxDDAKBgNVBAoMA0hJ
RDEMMAoGA1UECwwDTkFTMRYwFAYDVQQDDA1oaWRnbG9iYWwuY29tMSQwIgYJKoZI
hvcNAQkBFhVzdXBwb3J0QGhpZGdsb2JhbC5jb20wggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDSibuXB9Tn0EdwL2jDig26s/b1D9SX5B4xnZM+xZ4/mE6U
Meg5xbiTSMiWqtoSMVxG1WJDxogJWxCgZis2qk3AG89PBarg17pBmxPLYyCLricx
alyNvTJBxYgA/zKagPof6h6UqKOkhsW9qvulEmPe+TKk47pmlZXe+v+1A6PQDY5B
Y3MtqE23cnZ5nBTVanFAc1vbokMXUCCtRvE1Y/KhvuaJr2VjOSJ/KV3vcdTSCLGc
W9/n/Fv8udvI/eIkoPNpCUwngm8j3Aa7qN/OSg3SvVvBcl/Ykc08STSyZPMJGBaR
EuUcAraEBZbUDOCinDS488jKVHAXhrnvzzi7RMlhAgMBAAGjUDBOMB0GA1UdDgQW
BBSvOzxCjQi86ZUsuW0o4aa7GNAeSTAfBgNVHSMEGDAWgBSvOzxCjQi86ZUsuW0o
4aa7GNAeSTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA/8FCv6x8v
PHm2Ya/hbZ/S3amdl7/E1illeApNRZodTGCn/rlVSGanCfWYzEY2naHiDC2ImhZq
NkHK9uvUtxaVQFq5VN6WQMo351J78LLfcoqpKOLGX3b9byFvrw7WporZx3C7yL1U
LS3oxI/pgavxy1KbOIw/yl+QgV50vlfvQ7sKZ1E5YOrgWLP5nJ9OeEKRdsASJyZS
Jjl0k/eGaZreSvAZPmx4kaePfbi7DNDA+mNhSFygwt6AakjjVoF2xUZ1F+qwBtER
GPxdZWldywUYsdBRG1PPvBsMo9ME46HpPdXRIjMge8P01fsaMr/6H86ojWg9uJmH
cCKtiouo08hL
-----END CERTIFICATE-----

View File

@ -22,6 +22,7 @@ class Config:
self.DOOR_PASSWORD, self.DOOR_PASSWORD,
name=doorName, name=doorName,
access=doorData["access"], access=doorData["access"],
cert=self.doorControllerCA_BUNDLE
) )
for doorName, doorData in self.doorControllers.items() for doorName, doorData in self.doorControllers.items()
} }

View File

@ -3,9 +3,10 @@ from datetime import datetime
from io import StringIO from io import StringIO
import requests import requests
import urllib3
from lxml import etree from lxml import etree
from lxml.builder import ElementMaker from lxml.builder import ElementMaker
from requests import Session
from requests.adapters import HTTPAdapter
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"}) E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
E = ElementMaker( E = ElementMaker(
@ -22,9 +23,10 @@ fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDat
"," ","
) )
# TODO: where should this live?
# it's fine, ssl certs are for losers anyway class HostNameIgnoringAdapter(HTTPAdapter):
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def init_poolmanager(self, *args, **kwargs):
super().init_poolmanager(*args, **kwargs, assert_hostname=False)
class RemoteError(Exception): class RemoteError(Exception):
@ -33,12 +35,16 @@ class RemoteError(Exception):
class DoorController: class DoorController:
def __init__(self, ip, username, password, name="", access=""): def __init__(self, ip, username, password, name="", access="", cert=None):
self.ip = ip self.ip = ip
self.username = username self.username = username
self.password = password self.password = password
self.name = name self.name = name
self.access = access self.access = access
self.session = Session()
if cert is not None:
self.session.mount("https://", HostNameIgnoringAdapter())
self.session.verify = cert
# lazy evaluated, hopefully won't change for the lifetime of this object # lazy evaluated, hopefully won't change for the lifetime of this object
@property @property
@ -55,13 +61,12 @@ class DoorController:
def doImport(self, params=None, files=None): def doImport(self, params=None, files=None):
"""Send a request to the door control import script""" """Send a request to the door control import script"""
r = requests.post( r = self.session.post(
"https://" + self.ip + "/cgi-bin/import.cgi", "https://" + self.ip + "/cgi-bin/import.cgi",
params=params, params=params,
files=files, files=files,
auth=requests.auth.HTTPDigestAuth(self.username, self.password), auth=requests.auth.HTTPDigestAuth(self.username, self.password),
timeout=60, timeout=60,
verify=False,
) # ignore insecure SSL ) # ignore insecure SSL
xml = etree.XML(r.content) xml = etree.XML(r.content)
if ( if (
@ -82,11 +87,10 @@ class DoorController:
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'): def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
if not isinstance(xml, bytes): if not isinstance(xml, bytes):
xml = etree.tostring(xml) xml = etree.tostring(xml)
r = requests.get( r = self.session.get(
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi", "https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
params={"XML": prefix + xml}, params={"XML": prefix + xml},
auth=requests.auth.HTTPDigestAuth(self.username, self.password), auth=requests.auth.HTTPDigestAuth(self.username, self.password),
verify=False,
) )
resp_xml = etree.XML(r.content) resp_xml = etree.XML(r.content)
# probably meed to be more sane about this # probably meed to be more sane about this