Source code for request.message_responder.est
"""EST-specific message responder classes."""
import base64
from cryptography.hazmat.primitives.serialization import Encoding, pkcs7
from onboarding.models import OnboardingStatus
from request.request_context import BaseRequestContext, EstBaseRequestContext, EstCertificateRequestContext
from workflows.models import State
from .base import AbstractMessageResponder
[docs]
class EstMessageResponder(AbstractMessageResponder):
"""Builds response to EST requests."""
@staticmethod
[docs]
def build_response(context: BaseRequestContext) -> None:
"""Respond to an EST message."""
if not isinstance(context, EstBaseRequestContext):
exc_msg = 'EstMessageResponder requires a subclass of EstBaseRequestContext.'
raise TypeError(exc_msg)
if context.operation in ['simpleenroll', 'simplereenroll']:
responder = EstCertificateMessageResponder()
return responder.build_response(context)
exc_msg = 'No suitable responder found for this EST message.'
context.http_response_status = 500
context.http_response_content = exc_msg
return EstErrorMessageResponder().build_response(context)
[docs]
class EstCertificateMessageResponder(EstMessageResponder):
"""Respond to an EST enrollment message with the issued certificate."""
@staticmethod
def _check_workflow_state(context: EstCertificateRequestContext) -> bool:
"""Check if the workflow state allows for certificate issuance."""
if not context.enrollment_request:
exc_msg = 'No enrollment request is set in the context.'
raise ValueError(exc_msg)
workflow_state = context.enrollment_request.aggregated_state
if workflow_state == State.AWAITING:
context.http_response_status = 202
context.http_response_content_type = 'text/plain'
context.http_response_content = 'Enrollment request pending manual approval.'
# TODO(Air): Implement Retry-After header # noqa: FIX002
return False
if workflow_state == State.REJECTED:
context.enrollment_request.finalize(State.REJECTED)
context.http_response_status = 403
context.http_response_content_type = 'text/plain'
context.http_response_content = 'Enrollment request Rejected.'
return False
if workflow_state == State.FAILED:
context.http_response_status = 500
context.http_response_content_type = 'text/plain'
context.http_response_content = \
f'Workflow failed. Check here: -> /workflows/requests/{context.enrollment_request.id}'
return False
if not context.enrollment_request.is_valid():
context.http_response_status = 500
context.http_response_content_type = 'text/plain'
context.http_response_content = 'Enrollment request is not in a valid state for certificate issuance.'
return False
context.enrollment_request.finalize(State.FINALIZED)
return True
@staticmethod
def _prepare_certificate_data(context: EstCertificateRequestContext) -> tuple[str | bytes, str]:
"""Prepare the certificate data and content type based on encoding."""
if context.issued_certificate is None:
exc_msg = 'Issued certificate is not set in the context.'
raise ValueError(exc_msg)
encoding: Encoding = Encoding.PEM
if context.est_encoding in {'der', 'base64_der', 'pkcs7'}:
encoding = Encoding.DER
if context.est_encoding == 'pkcs7':
chain = [context.issued_certificate]
if context.issued_certificate_chain:
chain.extend(context.issued_certificate_chain)
cert_bytes = pkcs7.serialize_certificates(chain, encoding=Encoding.DER)
else:
cert_bytes = context.issued_certificate.public_bytes(encoding=encoding)
if context.est_encoding == 'der':
return cert_bytes, 'application/pkix-cert'
if context.est_encoding == 'pem':
cert: str | bytes
try:
cert = cert_bytes.decode('utf-8')
except UnicodeDecodeError:
cert = cert_bytes
return cert, 'application/x-pem-file'
# Default to RFC 7030 compliant format: base64-encoded with line wrapping
b64_cert = base64.b64encode(cert_bytes).decode('utf-8')
cert = '\n'.join([b64_cert[i:i + 64] for i in range(0, len(b64_cert), 64)]) + '\n'
if context.est_encoding == 'base64_der':
content_type = 'application/pkix-cert'
else:
content_type = 'application/pkcs7-mime; smime-type=certs-only'
return cert, content_type
@staticmethod
[docs]
def build_response(context: BaseRequestContext) -> None:
"""Respond to an EST enrollment message with the issued certificate."""
if not isinstance(context, EstCertificateRequestContext):
exc_msg = 'EstCertificateMessageResponder requires an EstCertificateRequestContext.'
raise TypeError(exc_msg)
if not EstCertificateMessageResponder._check_workflow_state(context):
return
if context.issued_certificate is None:
exc_msg = 'Issued certificate is not set in the context.'
raise ValueError(exc_msg)
cert, content_type = EstCertificateMessageResponder._prepare_certificate_data(context)
if context.device and context.device.onboarding_config:
context.device.onboarding_config.onboarding_status = OnboardingStatus.ONBOARDED
context.device.onboarding_config.save()
context.http_response_status = 200
context.http_response_content = cert
context.http_response_content_type = content_type
if context.est_encoding in {'pkcs7', 'base64_der'}:
context.http_response_headers = {'Content-Transfer-Encoding': 'base64'}
[docs]
class EstErrorMessageResponder(EstMessageResponder):
"""Respond to an EST message with an error."""
@staticmethod
[docs]
def build_response(context: BaseRequestContext) -> None:
"""Respond to an EST message with an error."""
# Set appropriate HTTP status code and error message in context
if not isinstance(context, EstBaseRequestContext):
exc_msg = 'EstErrorMessageResponder requires an EstBaseRequestContext.'
raise TypeError(exc_msg)
context.http_response_status = context.http_response_status or 500
context.http_response_content = context.http_response_content or 'An error occurred processing the EST request.'
context.http_response_content_type = context.http_response_content_type or 'text/plain'