acertmgr: Add a OCSP validation to certificate verification

This commit is contained in:
Kishi85 2020-03-04 13:47:32 +01:00
parent c33a39a433
commit b37d0cad94
5 changed files with 73 additions and 4 deletions

View File

@ -72,6 +72,7 @@ By default the directory (work_dir) containing the working data (csr,certificate
| account_key_algorithm | d,**g** | Key-algorithm for newly generated account keys (RSA, EC, ED25519, ED448) | RSA |
| account_key_length | d,**g** | Key-length for newly generated RSA account keys (in bits) or EC curve (256=P-256, 384=P-384, 521=P-521) | depends on account_key_algorithm |
| ttl_days | d,**g** | Renew certificate if it has less than this value validity left | 30 |
| validate_ocsp | d,**g** | Renew certificate if it's OCSP status is REVOKED. Allowed values for this key are: false, sha1, sha224, sha256, sha384, sha512 | sha1 (as mandated by RFC5019) |
| cert_dir | d,**g** | Directory containing all certificate related data (crt,key,csr) | {work_dir} |
| key_algorithm | d,**g** | Key-algorithm for newly generated private keys (RSA, EC, ED25519, ED448) | RSA |
| key_length | d,**g** | Key-length for newly generated RSA private keys (in bits) or EC curve (256=P-256, 384=P-384, 521=P-521) | depends on key_algorithm |

View File

@ -129,7 +129,8 @@ def cert_revoke(cert, configs, fallback_authority, reason=None):
if not acmeconfig:
acmeconfig = fallback_authority
log("No matching authority found to revoke {}: {}, using globalconfig/defaults".format(tools.get_cert_cn(cert),
tools.get_cert_domains(cert)), warning=True)
tools.get_cert_domains(
cert)), warning=True)
acme = authority(acmeconfig)
acme.register_account()
acme.revoke_crt(cert, reason)
@ -157,9 +158,21 @@ def main():
cert = None
if os.path.isfile(config['cert_file']):
cert = tools.read_pem_file(config['cert_file'])
if not cert or not tools.is_cert_valid(cert, config['ttl_days']) or (
'force_renew' in runtimeconfig and
all(d in config['domainlist'] for d in runtimeconfig['force_renew'])):
validate_ocsp = str(config.get('validate_ocsp')).lower() != 'false'
if validate_ocsp and cert and os.path.isfile(config['ca_file']):
try:
issuer = tools.read_pem_file(config['ca_file'])
except Exception as e1:
log("Failed to retrieve issuer from ca file: {}. Trying to download...".format(e1))
try:
issuer = tools.download_issuer_ca(cert)
except Exception as e2:
log("Failed to download issuer for cert file: {}. Cannot validate OCSP.".format(e2))
validate_ocsp = False
if not cert or ('force_renew' in runtimeconfig and all(
d in config['domainlist'] for d in runtimeconfig['force_renew'])) \
or not tools.is_cert_valid(cert, config['ttl_days']) \
or (validate_ocsp and not tools.is_ocsp_valid(cert, issuer, config['validate_ocsp'])):
cert_get(config)
if str(config.get('cert_revoke_superseded')).lower() == 'true' and cert:
superseded.add(cert)

View File

@ -19,6 +19,7 @@ from acertmgr.tools import idna_convert
DEFAULT_CONF_DIR = "/etc/acertmgr"
DEFAULT_CONF_FILENAME = "acertmgr.conf"
DEFAULT_TTL = 30 # days
DEFAULT_VALIDATE_OCSP = "sha1" # mandated by RFC5019
DEFAULT_API = "v2"
DEFAULT_AUTHORITY = "https://acme-v02.api.letsencrypt.org"
@ -106,6 +107,9 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
update_config_value(config, 'ttl_days', localconfig, globalconfig, DEFAULT_TTL)
config['ttl_days'] = int(config['ttl_days'])
# Validate OCSP on certificate verification
update_config_value(config, 'validate_ocsp', localconfig, globalconfig, DEFAULT_VALIDATE_OCSP)
# Revoke old certificate with reason superseded after renewal
update_config_value(config, 'cert_revoke_superseded', localconfig, globalconfig, "false")

View File

@ -22,6 +22,11 @@ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from cryptography.utils import int_to_bytes
from cryptography.x509.oid import NameOID, ExtensionOID
try:
from cryptography.x509 import ocsp
except ImportError:
pass
try:
from cryptography.hazmat.primitives.asymmetric import ed25519, ed448
except ImportError:
@ -388,3 +393,48 @@ def idna_convert(domainlist):
if any(ord(c) >= 128 for c in ''.join(domainlist)) and 'idna' not in sys.modules:
log("Unicode domain(s) found but IDNA names could not be translated due to missing idna module", error=True)
return [(x, x) for x in domainlist]
# @brief validate the OCSP status for a given certificate by the given issuer
def is_ocsp_valid(cert, issuer, hash_algo):
if hash_algo == 'sha1':
algorithm = hashes.SHA1
elif hash_algo == 'sha224':
algorithm = hashes.SHA224
elif hash_algo == 'sha256':
algorithm = hashes.SHA256
elif hash_algo == 'sha385':
algorithm = hashes.SHA384
elif hash_algo == 'sha512':
algorithm = hashes.SHA512
else:
log("Invalid hash algorithm '{}' used for OCSP validation. Validation ignored.".format(hash_algo), warning=True)
return True
try:
ocsp_urls = []
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
for data in aia.value:
if data.access_method == x509.OID_OCSP:
ocsp_urls.append(data.access_location.value)
# This is a bit of a hack due to validation problems within cryptography (TODO: Check if this is still true)
# Correct replacement: ocsprequest = ocsp.OCSPRequestBuilder().add_certificate(cert, issuer, algorithm).build()
ocsprequest = ocsp.OCSPRequestBuilder((cert, issuer, algorithm)).build()
ocsprequestdata = ocsprequest.public_bytes(serialization.Encoding.DER)
for ocsp_url in ocsp_urls:
response = get_url(ocsp_url,
ocsprequestdata,
{
'Accept': 'application/ocsp-response',
'Content-Type': 'application/ocsp-request',
})
ocspresponsedata = response.read()
ocspresponse = ocsp.load_der_ocsp_response(ocspresponsedata)
if ocspresponse.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL \
and ocspresponse.certificate_status == ocsp.OCSPCertStatus.REVOKED:
return False
except Exception as e:
log("An exception occurred during OCSP validation (Validation will be ignored): {}".format(e), error=True)
return True

View File

@ -50,6 +50,7 @@ extra_requirements = {
"yaml": ["PyYAML"],
"idna": ["idna"],
"ocsp-must-staple": ["cryptography>=2.1"],
"ocsp-validation": ["cryptography>=2.4"],
"ed25519": ["cryptography>=2.6"],
}