import falcon import logging import os from asn1crypto.util import timezone from asn1crypto import ocsp from base64 import b64decode from certidude import config, const from datetime import datetime, timedelta from oscrypto import asymmetric from .utils import AuthorityHandler from .utils.firewall import whitelist_subnets logger = logging.getLogger(__name__) class OCSPResource(AuthorityHandler): @whitelist_subnets(config.OCSP_SUBNETS) def __call__(self, req, resp): try: if req.method == "GET": _, _, _, tail = req.path.split("/", 3) body = b64decode(tail) elif req.method == "POST": body = req.stream.read(req.content_length or 0) else: raise falcon.HTTPMethodNotAllowed() ocsp_req = ocsp.OCSPRequest.load(body) except ValueError: raise falcon.HTTPBadRequest() fh = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") # TODO: import from authority server_certificate = asymmetric.load_certificate(fh.read()) fh.close() now = datetime.now(timezone.utc) response_extensions = [] try: for ext in ocsp_req["tbs_request"]["request_extensions"]: if ext["extn_id"].native == "nonce": response_extensions.append( ocsp.ResponseDataExtension({ 'extn_id': "nonce", 'critical': False, 'extn_value': ext["extn_value"] }) ) except ValueError: # https://github.com/wbond/asn1crypto/issues/56 pass responses = [] for item in ocsp_req["tbs_request"]["request_list"]: serial = item["req_cert"]["serial_number"].native assert serial > 0, "Serial number correctness check failed" try: link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial)) assert link_target.startswith("../") assert link_target.endswith(".pem") path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4]) if serial != cert.serial_number: logger.error("Certificate store integrity check failed, %s refers to certificate with serial %040x", link_target, cert.serial_number) raise EnvironmentError("Integrity check failed") logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'good'", req.context.get("remote_addr"), cert.subject.native["common_name"], serial) status = ocsp.CertStatus(name='good', value=None) except EnvironmentError: try: path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial) logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'revoked' due to %s", req.context.get("remote_addr"), cert.subject.native["common_name"], serial, reason) status = ocsp.CertStatus( name='revoked', value={ 'revocation_time': revoked, 'revocation_reason': reason, }) except EnvironmentError: logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr")) status = ocsp.CertStatus(name="unknown", value=None) responses.append({ 'cert_id': { 'hash_algorithm': { 'algorithm': "sha1" }, 'issuer_name_hash': server_certificate.asn1.subject.sha1, 'issuer_key_hash': server_certificate.public_key.asn1.sha1, 'serial_number': serial, }, 'cert_status': status, 'this_update': now - const.CLOCK_SKEW_TOLERANCE, 'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE, 'single_extensions': [] }) response_data = ocsp.ResponseData({ 'responder_id': ocsp.ResponderId(name='by_key', value=server_certificate.public_key.asn1.sha1), 'produced_at': now, 'responses': responses, 'response_extensions': response_extensions }) resp.body = ocsp.OCSPResponse({ 'response_status': "successful", 'response_bytes': { 'response_type': "basic_ocsp_response", 'response': { 'tbs_response_data': response_data, 'certs': [server_certificate.asn1], 'signature_algorithm': {'algorithm': "sha1_ecdsa" if self.authority.public_key.algorithm == "ec" else "sha1_rsa" }, 'signature': (asymmetric.ecdsa_sign if self.authority.public_key.algorithm == "ec" else asymmetric.rsa_pkcs1v15_sign)( self.authority.private_key, response_data.dump(), "sha1" ) } } }).dump() # Interestingly openssl's OCSP code doesn't care about content type resp.append_header("Content-Type", "application/ocsp-response")