Source code for aoki.views
"""This module contains the AOKI endpoints (views)."""
from __future__ import annotations
import base64
from typing import TYPE_CHECKING, Any
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
from django.http import JsonResponse
from django.views import View
from trustpoint_core.oid import AlgorithmIdentifier
from pki.models.credential import CredentialModel, IDevIDReferenceModel
from pki.models.truststore import ActiveTrustpointTlsServerCredentialModel
from pki.util.idevid import IDevIDAuthenticationError, IDevIDAuthenticator
from pki.util.x509 import ClientCertificateAuthenticationError, NginxTLSClientCertExtractor
from trustpoint.logger import LoggerMixin
from trustpoint.views.base import LoggedHttpResponse
if TYPE_CHECKING:
from django.http import HttpRequest
[docs]
class AokiServiceMixin:
"""Mixin for AOKI functionality."""
@staticmethod
[docs]
def get_idevid_owner_san_uri(idevid_cert: x509.Certificate) -> str:
"""Get the Owner ID SAN URI corresponding to a IDevID certificate.
Formatted as 'dev-owner:<IDevID_Subj_SN>.<IDevID_x509_SN>.<IDevID_SHA256_Fingerpr>'
"""
try:
sn_b = idevid_cert.subject.get_attributes_for_oid(x509.NameOID.SERIAL_NUMBER)[0].value
idevid_subj_sn = sn_b.decode() if isinstance(sn_b, bytes) else sn_b
except (ValueError, IndexError):
idevid_subj_sn = '_'
idevid_x509_sn = f'{idevid_cert.serial_number:x}'.zfill(16)
idevid_sha256_fingerprint = idevid_cert.fingerprint(hashes.SHA256()).hex()
return f'dev-owner:{idevid_subj_sn}.{idevid_x509_sn}.{idevid_sha256_fingerprint}'
@staticmethod
[docs]
def get_owner_credential(idevid_cert: x509.Certificate) -> CredentialModel | None:
"""Get the Device Owner ID credential corresponding to a IDevID cert, or None if it does not exist in the DB.
This does not perform any authentication or validation of the IDevID certificate! Use IDevIDAuthenticator first.
"""
idevid_san_uri = AokiServiceMixin.get_idevid_owner_san_uri(idevid_cert)
owner_cred_ref = IDevIDReferenceModel.objects.filter(idevid_ref=idevid_san_uri).first()
if not owner_cred_ref:
return None
owner_cred = owner_cred_ref.dev_owner_id
issued = owner_cred.dev_owner_id_credential
if issued is None:
return None
return issued.credential
[docs]
class AokiInitializationRequestView(AokiServiceMixin, LoggerMixin, View):
"""View for handling AOKI initialization requests."""
[docs]
http_method_names = ('get',)
[docs]
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> LoggedHttpResponse | JsonResponse:
"""Handle GET requests for AOKI initialization."""
del args, kwargs # Unused
tls_cert = ActiveTrustpointTlsServerCredentialModel.objects.first()
if not tls_cert or not tls_cert.credential:
return LoggedHttpResponse(
'No TLS server certificate available. Are you on the development server?', status = 500
)
try:
client_cert, intermediary_cas = NginxTLSClientCertExtractor.get_client_cert_as_x509(request)
except ClientCertificateAuthenticationError:
return LoggedHttpResponse(
'No valid TLS client certificate provided.', status = 401
)
try:
domain, _idevid_subj_sn = IDevIDAuthenticator.authenticate_idevid_from_x509_no_device(
client_cert, intermediary_cas, domain=None)
except IDevIDAuthenticationError:
self.logger.exception('IDevID authentication failed.')
return LoggedHttpResponse(
'IDevID authentication failed.', status = 403
)
owner_cred = self.get_owner_credential(client_cert)
if not owner_cred:
return LoggedHttpResponse(
'No DevOwnerID present for this IDevID.', status = 422
)
owner_pk = owner_cred.get_private_key()
try:
owner_id_cert = owner_cred.certificate_or_error
owner_cert_pem = owner_id_cert.get_certificate_serializer().as_pem().decode()
except ValueError:
self.logger.warning('Owner credential has no certificate')
owner_cert_pem = ''
try:
tls_cert_pem = tls_cert.credential.certificate_or_error.get_certificate_serializer().as_pem().decode()
except ValueError:
self.logger.warning('TLS credential has no certificate')
tls_cert_pem = ''
aoki_init_response = {
'aoki-init': {
'version': '1.0',
'enrollment-info': {
'protocols': [
{
'protocol': 'EST',
'url': f'/.well-known/est/{domain.unique_name}/domain_credential/'
}
]
},
'owner-id-cert': owner_cert_pem,
'tls-truststore': tls_cert_pem
},
}
resp = JsonResponse(aoki_init_response)
if isinstance(owner_pk, rsa.RSAPrivateKey):
owner_signature = owner_pk.sign(
data=resp.content,
padding=padding.PKCS1v15(),
algorithm=hashes.SHA256(),
)
signature_algo = AlgorithmIdentifier.RSA_SHA256.dotted_string
elif isinstance(owner_pk, ec.EllipticCurvePrivateKey):
owner_signature = owner_pk.sign(
data=resp.content,
signature_algorithm=ec.ECDSA(hashes.SHA256()),
)
signature_algo = AlgorithmIdentifier.ECDSA_SHA256.dotted_string
else:
exc_msg = f'Unsupported private key type: {type(owner_pk)} for AOKI owner signing.'
raise TypeError(exc_msg)
resp.headers['AOKI-Signature'] = base64.b64encode(owner_signature).decode()
resp.headers['AOKI-Signature-Algorithm'] = signature_algo
self.logger.info(resp.content)
return resp