feature: pki.extended_key_usages adds custom extended key usages by their OIDs, which templates give by name, and a template's policies put certificate policy OIDs into its certificates.
Release / release (push) Successful in 20s

This commit is contained in:
slawek committed 2026-10-25 23:27:19 +01:00
1 parent a245ec26cd
commit d648d294ec
6 files changed
+92 -33

No files matched your search

@@ -57,7 +57,7 @@ SCHEMA = {
'pki': {'root_name': 'VPN Router Root CA', 'root_days': 7300, 'ca_days': 3650, 'key': 'rsa-4096',
'crl_days': 7, 'root_crl_days': 180, 'publish': ['http'], 'p2s_name': 'VPN Router P2S CA',
's2s_name': 'VPN Router Site-to-Site CA', 'issuing_cas': {}, 'templates': {},
'root_key_warning': True},
'extended_key_usages': {}, 'root_key_warning': True},
'metrics': {'enabled': True, 'listen': 'internal', 'port': 9464, 'https_port': 9465, 'service_account': 'metrics',
'otlp_endpoint': '', 'otlp_username': '', 'otlp_password': '',
'keep_details': '24 hours', 'keep_hourly': '1 month', 'keep_daily': '3 months',
@@ -136,9 +136,23 @@ PKI_PUBLISH = ('http', 'http-80', 'https', 'https-default')
#: The key usages a template may give, as the arguments of cryptography's KeyUsage.
KEY_USAGES = ('digital_signature', 'content_commitment', 'key_encipherment', 'data_encipherment', 'key_agreement')
#: The extended key usages a template may give.
EXTENDED_KEY_USAGES = ('server_auth', 'client_auth', 'code_signing', 'email_protection', 'time_stamping',
'ocsp_signing', 'ipsec_ike', 'ike_intermediate')
#: The built-in extended key usages a template may give, name -> OID; pki.extended_key_usages adds custom ones.
EXTENDED_KEY_USAGES = {
'server_auth': '1.3.6.1.5.5.7.3.1',
'client_auth': '1.3.6.1.5.5.7.3.2',
'code_signing': '1.3.6.1.5.5.7.3.3',
'email_protection': '1.3.6.1.5.5.7.3.4',
'time_stamping': '1.3.6.1.5.5.7.3.8',
'ocsp_signing': '1.3.6.1.5.5.7.3.9',
'ipsec_ike': '1.3.6.1.5.5.7.3.17',
'ike_intermediate': '1.3.6.1.5.5.8.2.2',
}
#: The name of a custom extended key usage of pki.extended_key_usages.
EKU_NAME = re.compile(r'^[a-z][a-z0-9_-]{0,30}$')
#: An object identifier in dotted form, of a custom extended key usage or a certificate policy.
OID = re.compile(r'^[0-2](\.(0|[1-9][0-9]*)){1,63}$')
#: What the common name of a template's certificates is: a DNS name, any name, or an e-mail address.
TEMPLATE_SUBJECTS = ('dns', 'name', 'email')
@@ -148,7 +162,7 @@ SAN_TYPES = ('dns', 'ip', 'email')
#: A certificate template: option -> default, the type of the default being the required type.
TEMPLATE = {'key': 'rsa-3072', 'days': 365, 'key_usage': ['digital_signature'], 'extended_key_usage': [],
'subject': 'name', 'sans': []}
'subject': 'name', 'sans': [], 'policies': []}
#: The built-in templates; pki.templates changes their options and adds templates of its own.
PKI_TEMPLATES = {
@@ -502,6 +516,14 @@ def validate(cfg):
raise ConfigError('pki.publish: http is served by the web service, which needs web.enabled')
if set(publish) - {'http'} and not _get(cfg, 'https', 'enabled'):
raise ConfigError('pki.publish: http-80, https and https-default are served by Caddy, which needs https.enabled')
for name, oid in cfg['pki']['extended_key_usages'].items():
prefix = f'pki.extended_key_usages.{name}'
if not EKU_NAME.match(name) or name in EXTENDED_KEY_USAGES:
raise ConfigError(f'{prefix}: an extended key usage name starts with a lowercase letter, has at most 31 '
'lowercase letters, digits, hyphens and underscores, and is not a built-in one')
if not isinstance(oid, str) or not OID.match(oid):
raise ConfigError(f'{prefix} must be an OID in dotted form, such as 1.3.6.1.4.1.311.20.2.2')
usages = extended_key_usages(cfg)
for name, template in cfg['pki']['templates'].items():
prefix = f'pki.templates.{name}'
if not PKI_NAME.match(name):
@@ -517,11 +539,14 @@ def validate(cfg):
raise ConfigError(f'{prefix}.key: {template["key"]!r} is not one of {", ".join(PKI_KEYS)}')
if not 1 <= template['days'] <= 36500:
raise ConfigError(f'{prefix}.days must be 1 to 36500 days')
for option, allowed in (('key_usage', KEY_USAGES), ('extended_key_usage', EXTENDED_KEY_USAGES),
for option, allowed in (('key_usage', KEY_USAGES), ('extended_key_usage', tuple(usages)),
('sans', SAN_TYPES)):
for item in template[option]:
if item not in allowed:
raise ConfigError(f'{prefix}.{option}: {item!r} is not one of {", ".join(allowed)}')
for item in template['policies']:
if not OID.match(item):
raise ConfigError(f'{prefix}.policies: {item!r} is not an OID in dotted form, such as 1.3.6.1.4.1.99999.1')
if not template['key_usage']:
raise ConfigError(f'{prefix}.key_usage needs at least one usage')
if template['subject'] not in TEMPLATE_SUBJECTS:
@@ -665,6 +690,13 @@ def pki_templates(cfg):
for name in [*PKI_TEMPLATES, *(name for name in custom if name not in PKI_TEMPLATES)]}
def extended_key_usages(cfg):
"""The extended key usages templates may give, name -> OID: the built-in ones (EXTENDED_KEY_USAGES) and those of
pki.extended_key_usages."""
custom = cfg['pki']['extended_key_usages']
return {**EXTENDED_KEY_USAGES, **{name: oid for name, oid in custom.items() if isinstance(oid, str)}}
def issuing_cas(cfg):
"""The issuing CAs, name -> {name, templates, acme, builtin}: p2s and s2s (BUILTIN_CAS) with their common names from
the pki section, then those of pki.issuing_cas, each filled from ISSUING_CA."""