197 lines
7.3 KiB
Python
197 lines
7.3 KiB
Python
from dataclasses import dataclass
|
|
from typing import Optional, Set
|
|
|
|
from asn1crypto import x509
|
|
from pyhanko_certvalidator.errors import InvalidCertificateError
|
|
|
|
from pyhanko.config.api import (
|
|
ConfigurableMixin,
|
|
process_bit_string_flags,
|
|
process_oids,
|
|
)
|
|
|
|
|
|
def _match_usages(required: set, present: set, need_all: bool):
|
|
if need_all:
|
|
return not (required - present)
|
|
else:
|
|
# intersection must be non-empty
|
|
return bool(required & present)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class KeyUsageConstraints(ConfigurableMixin):
|
|
"""
|
|
Convenience class to pass around key usage requirements and validate them.
|
|
Intended to be flexible enough to handle both PKIX and ISO 32000 certificate
|
|
seed value constraint semantics.
|
|
|
|
.. versionchanged:: 0.6.0
|
|
Bring extended key usage semantics in line with :rfc:`5280` (PKIX).
|
|
"""
|
|
|
|
key_usage: Optional[Set[str]] = None
|
|
"""
|
|
All or some (depending on :attr:`match_all_key_usage`) of these key usage
|
|
extensions must be present in the signer's certificate.
|
|
If not set or empty, all key usages are considered acceptable.
|
|
"""
|
|
|
|
key_usage_forbidden: Optional[Set[str]] = None
|
|
"""
|
|
These key usages must not be present in the signer's certificate.
|
|
|
|
.. note::
|
|
This behaviour is undefined in :rfc:`5280` (PKIX), but included for
|
|
compatibility with certificate seed value settings in ISO 32000.
|
|
"""
|
|
|
|
extd_key_usage: Optional[Set[str]] = None
|
|
"""
|
|
List of acceptable key purposes that can appear in an extended key
|
|
usage extension in the signer's certificate, if such an extension is at all
|
|
present. If not set, all extended key usages are considered acceptable.
|
|
|
|
If no extended key usage extension is present, or if the
|
|
``anyExtendedKeyUsage`` key purpose ID is present, the resulting behaviour
|
|
depends on :attr:`explicit_extd_key_usage_required`.
|
|
|
|
Setting this option to the empty set (as opposed to ``None``) effectively
|
|
bans all (presumably unrecognised) extended key usages.
|
|
|
|
.. warning::
|
|
Note the difference in behaviour with :attr:`key_usage` for empty
|
|
sets of valid usages.
|
|
|
|
.. warning::
|
|
Contrary to what some CAs seem to believe, the criticality of the
|
|
extended key usage extension is irrelevant here.
|
|
Even a non-critical EKU extension **must** be enforced according to
|
|
:rfc:`5280` § 4.2.1.12.
|
|
|
|
In practice, many certificate authorities issue non-repudiation certs
|
|
that can also be used for TLS authentication by only including the
|
|
TLS client authentication key purpose ID in the EKU extension.
|
|
Interpreted strictly, :rfc:`5280` bans such certificates from being
|
|
used to sign documents, and pyHanko will enforce these semantics
|
|
if :attr:`extd_key_usage` is not ``None``.
|
|
"""
|
|
|
|
explicit_extd_key_usage_required: bool = True
|
|
"""
|
|
.. versionadded:: 0.6.0
|
|
|
|
Require an extended key usage extension with the right key usages to be
|
|
present if :attr:`extd_key_usage` is non-empty.
|
|
|
|
If this flag is ``True``, at least one key purpose in :attr:`extd_key_usage`
|
|
must appear in the certificate's extended key usage, and
|
|
``anyExtendedKeyUsage`` will be ignored.
|
|
"""
|
|
|
|
match_all_key_usages: bool = False
|
|
"""
|
|
.. versionadded:: 0.6.0
|
|
|
|
If ``True``, all key usages indicated in :attr:`key_usage` must be present
|
|
in the certificate. If ``False``, one match suffices.
|
|
|
|
If :attr:`key_usage` is empty or ``None``, this option has no effect.
|
|
"""
|
|
|
|
def validate(self, cert: x509.Certificate):
|
|
self._validate_key_usage(cert.key_usage_value)
|
|
self._validate_extd_key_usage(cert.extended_key_usage_value)
|
|
|
|
def _validate_key_usage(self, key_usage_extension_value):
|
|
if not self.key_usage:
|
|
return
|
|
key_usage = self.key_usage or set()
|
|
key_usage_forbidden = self.key_usage_forbidden or set()
|
|
|
|
# First, check the "regular" key usage extension
|
|
cert_ku = (
|
|
set(key_usage_extension_value.native)
|
|
if key_usage_extension_value is not None
|
|
else set()
|
|
)
|
|
|
|
# check blacklisted key usages (ISO 32k)
|
|
forbidden_ku = cert_ku & key_usage_forbidden
|
|
if forbidden_ku:
|
|
rephrased = map(lambda s: s.replace('_', ' '), forbidden_ku)
|
|
raise InvalidCertificateError(
|
|
"The active key usage policy explicitly bans certificates "
|
|
f"used for {', '.join(rephrased)}."
|
|
)
|
|
|
|
# check required key usage extension values
|
|
need_all_ku = self.match_all_key_usages
|
|
if not _match_usages(key_usage, cert_ku, need_all_ku):
|
|
rephrased = map(lambda s: s.replace('_', ' '), key_usage)
|
|
raise InvalidCertificateError(
|
|
"The active key usage policy requires "
|
|
f"{'' if need_all_ku else 'at least one of '}the key "
|
|
f"usage extensions {', '.join(rephrased)} to be present."
|
|
)
|
|
|
|
def _validate_extd_key_usage(self, eku_extension_value):
|
|
if self.extd_key_usage is None:
|
|
return
|
|
# check extended key usage
|
|
has_extd_key_usage_ext = eku_extension_value is not None
|
|
cert_eku = (
|
|
set(eku_extension_value.native) if has_extd_key_usage_ext else set()
|
|
)
|
|
|
|
if (
|
|
'any_extended_key_usage' in cert_eku
|
|
and not self.explicit_extd_key_usage_required
|
|
):
|
|
return # early out, cert is valid for all EKUs
|
|
|
|
extd_key_usage = self.extd_key_usage or set()
|
|
if not has_extd_key_usage_ext:
|
|
if self.explicit_extd_key_usage_required:
|
|
raise InvalidCertificateError(
|
|
"The active key usage policy requires an extended "
|
|
"key usage extension."
|
|
)
|
|
return # early out, cert is (presumably?) valid for all EKUs
|
|
|
|
if not _match_usages(extd_key_usage, cert_eku, need_all=False):
|
|
if extd_key_usage:
|
|
rephrased = map(lambda s: s.replace('_', ' '), extd_key_usage)
|
|
ok_list = f"Relevant key purposes are {', '.join(rephrased)}."
|
|
else:
|
|
ok_list = "There are no acceptable extended key usages."
|
|
raise InvalidCertificateError(
|
|
"The extended key usages for which this certificate is valid "
|
|
f"do not match the active key usage policy. {ok_list}"
|
|
)
|
|
|
|
@classmethod
|
|
def process_entries(cls, config_dict):
|
|
super().process_entries(config_dict)
|
|
|
|
# Deal with KeyUsage values first
|
|
# might as well expose key_usage_forbidden while we're at it
|
|
for key_usage_sett in ('key_usage', 'key_usage_forbidden'):
|
|
affected_flags = config_dict.get(key_usage_sett, None)
|
|
if affected_flags is not None:
|
|
config_dict[key_usage_sett] = set(
|
|
process_bit_string_flags(
|
|
x509.KeyUsage,
|
|
affected_flags,
|
|
key_usage_sett.replace('_', '-'),
|
|
)
|
|
)
|
|
|
|
extd_key_usage = config_dict.get('extd_key_usage', None)
|
|
if extd_key_usage is not None:
|
|
config_dict['extd_key_usage'] = set(
|
|
process_oids(
|
|
x509.KeyPurposeId, extd_key_usage, 'extd-key-usage'
|
|
)
|
|
)
|