System: Trust: Authorities - finish crl fetch script for installed certificates and hook into form and cron updates.

According to rfc5280 an CRL update should be issued before "next update", but does not seem to define a validity. Various sources seem to indicate updates should be fetched at least every couple of hours, in which case an hourly update sounds reasonable.

To avoid excessive writes, we compare each CRL offered with the last one received before flushing it to disk and keep track of actual changes. When nothing changes, no rehash is neede, which the configd action takes care of.

Distribution points could either be HTTP or LDAP, LDAPS and HTTPS may not be used according to the RFC (CAs SHOULD NOT include URIs that specify https, ldaps, or similar schemes in extensions.)
This commit is contained in:
Ad Schellevis 2024-09-24 20:58:04 +02:00
parent 6f79579537
commit b709232e44
6 changed files with 59 additions and 13 deletions

View File

@ -344,6 +344,10 @@ function core_cron()
}
}
if (!empty((string)(new OPNsense\Trust\General())->fetch_crls)) {
$jobs[]['autocron'] = array('/usr/local/sbin/configctl -d system trust download_crls', '1');
}
return $jobs;
}

View File

@ -44,6 +44,8 @@ class SettingsController extends ApiMutableModelControllerBase
{
if ($this->request->isPost()) {
(new Backend())->configdRun('system trust configure', true);
/* CRL fetches are scheduled */
(new Backend())->configdRun('cron restart', true);
return ['status' => 'ok'];
}
return ['status' => 'failed'];

View File

@ -17,11 +17,16 @@
<type>checkbox</type>
<help>Store all configured CRL's in the default trust store.</help>
</field>
<field>
<id>trust.fetch_crls</id>
<label>Auto fetch CRL's</label>
<type>checkbox</type>
<help>Schedule an hourly job to download CRLs using the defined Distributionpoints in the CAs deployed in our trust store.</help>
</field>
<field>
<type>header</type>
<label>Configuration constraints</label>
</field>
<field>
<id>trust.enable_legacy_sect</id>
<label>Enable legacy</label>

View File

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/trust/general</mount>
<description>Trust general settings</description>
<version>1.0.0</version>
<version>1.0.1</version>
<items>
<store_intermediate_certs type="BooleanField">
<Default>0</Default>
@ -11,6 +11,10 @@
<Default>0</Default>
<Required>Y</Required>
</install_crls>
<fetch_crls type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</fetch_crls>
<enable_legacy_sect type="BooleanField">
<Default>1</Default>
<Required>Y</Required>

View File

@ -23,9 +23,14 @@
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------------------------------------
Simple CRL Distributionpoint downloader using the CA's configured in the central trust store
Script returns exit status 0 when nothing has changed, 1 when changes have been made so a rehash can be scheduled
"""
import glob
import hashlib
import urllib
import os
import subprocess
import ldap3
import requests
@ -39,7 +44,7 @@ TRUSTPATH = ['/usr/share/certs/trusted', '/usr/local/share/certs', '/usr/local/e
def fetch_crl(uri):
p = urllib.parse.urlparse(uri)
payload = None
if p.scheme and p.scheme.lower() == 'ldap':
if p.scheme.lower() == 'ldap':
server = ldap3.Server(p.netloc)
conn = ldap3.Connection(server, auto_bind=True)
conn.search(
@ -53,7 +58,7 @@ def fetch_crl(uri):
if value and key.split(';')[0].lower() == 'certificaterevocationlist':
payload = value[0]
break
elif p.scheme and p.scheme.lower() == 'http':
elif p.scheme.lower() == 'http':
r = requests.get(uri)
if r.status_code >= 200 and r.status_code < 300:
payload = r.content
@ -76,7 +81,12 @@ def fetch_crl(uri):
'pem': crl.public_bytes(serialization.Encoding.PEM).decode()
}
def main():
changes = 0
output_pattern = '/usr/local/share/certs/ca-crl-upd-opn-%s.crl'
crl_files = []
dp_uri = ''
for path in TRUSTPATH:
for filename in glob.glob('%s/*[.pem|.crt]' % path):
try:
@ -84,7 +94,10 @@ def main():
for ext in cert.extensions:
if type(ext.value) is CRLDistributionPoints:
for Distributionpoint in ext.value:
this_crl = fetch_crl(Distributionpoint.full_name[0].value)
dp_uri = Distributionpoint.full_name[0].value
target_filename = output_pattern % hashlib.sha256(dp_uri.encode()).hexdigest()
this_crl = fetch_crl(dp_uri)
crl_files.append(target_filename)
# use local trust store to validate if the received CRL is valid
sp = subprocess.run(
['/usr/local/bin/openssl', 'crl', '-verify'],
@ -93,16 +106,28 @@ def main():
text=True
)
if sp.stderr.strip() == 'verify OK':
print('ok')
print(filename)
print(this_crl)
sys.exit(0)
#print(Distributionpoint.full_name[0].value)
if os.path.isfile(target_filename):
if open(target_filename).read() == this_crl['pem']:
print('[-] skip unchanged crl from %s' % dp_uri)
continue
with open(target_filename, 'w') as f_out:
print('[+] store crl from %s' % dp_uri)
f_out.write(this_crl['pem'])
changes += 1
else:
print('[-] skip crl from %s (%s)' % (dp_uri, sp.stderr.strip()))
except Exception as e:
# error handling
print(e)
print('[-] error processing %s [%s]' % (dp_uri, e))
# cleanup unused CRLs within our responsible scope
for filename in glob.glob(output_pattern % '*'):
if filename not in crl_files:
os.unlink(filename)
changes += 1
return changes
if __name__ == '__main__':
main()
sys.exit(0 if main() == 0 else 1)

View File

@ -146,6 +146,12 @@ parameters:
type:script
message:configure trust
[trust.download_crls]
command:/usr/local/opnsense/scripts/system/crl_fetch.py || /usr/local/opnsense/scripts/system/certctl.py rehash
parameters:
type:script
message:download CRLs from Distributionpoints
[trust.crl]
command:/usr/local/sbin/pluginctl -c crl
type:script