certidude/certidude/api/ocsp.py

123 lines
5.4 KiB
Python
Raw Normal View History

import falcon
2018-02-03 11:19:50 +00:00
import logging
2017-05-25 19:20:29 +00:00
import os
from asn1crypto.util import timezone
2018-02-03 11:22:12 +00:00
from asn1crypto import ocsp
from base64 import b64decode
from certidude import config, const
from datetime import datetime, timedelta
2018-02-03 11:22:12 +00:00
from oscrypto import asymmetric
from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets
2017-05-25 19:20:29 +00:00
2018-02-03 11:19:50 +00:00
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
2017-05-25 19:20:29 +00:00
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
2017-05-25 19:20:29 +00:00
responses = []
for item in ocsp_req["tbs_request"]["request_list"]:
serial = item["req_cert"]["serial_number"].native
2018-04-13 07:57:49 +00:00
assert serial > 0, "Serial number correctness check failed"
2017-05-25 19:20:29 +00:00
try:
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial))
2017-05-25 19:20:29 +00:00
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)
2017-05-25 19:20:29 +00:00
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)
2017-05-25 19:20:29 +00:00
status = ocsp.CertStatus(
name='revoked',
value={
'revocation_time': revoked,
'revocation_reason': reason,
2017-05-25 19:20:29 +00:00
})
except EnvironmentError:
logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr"))
2017-05-25 19:20:29 +00:00
status = ocsp.CertStatus(name="unknown", value=None)
responses.append({
'cert_id': {
'hash_algorithm': {
'algorithm': "sha1"
2017-05-25 19:20:29 +00:00
},
'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,
2017-05-25 19:20:29 +00:00
'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",
2017-05-25 19:20:29 +00:00
'response_bytes': {
'response_type': "basic_ocsp_response",
2017-05-25 19:20:29 +00:00
'response': {
'tbs_response_data': response_data,
'certs': [server_certificate.asn1],
2018-04-13 07:56:05 +00:00
'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"
)
2017-05-25 19:20:29 +00:00
}
}
}).dump()
2018-05-29 09:06:07 +00:00
# Interestingly openssl's OCSP code doesn't care about content type
resp.append_header("Content-Type", "application/ocsp-response")