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
Release / release (push) Successful in 20s
This commit is contained in:
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."""
|
||||
|
||||
@@ -44,7 +44,7 @@ from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from cryptography.x509.oid import AuthorityInformationAccessOID, ExtendedKeyUsageOID, NameOID
|
||||
from cryptography.x509.oid import AuthorityInformationAccessOID, NameOID
|
||||
|
||||
import vpnrouter
|
||||
import vpnrouter_ldap
|
||||
@@ -89,18 +89,6 @@ CERTIFICATE_ATTRIBUTES = ['serialNumber', 'cn', 'vpnRouterKind', 'vpnRouterTempl
|
||||
#: The suffix of a retired CA's name: -<time of the retirement>.
|
||||
RETIRED_SUFFIX = re.compile(r'-\d{14}$')
|
||||
|
||||
#: The extended key usages of vpnrouter.EXTENDED_KEY_USAGES.
|
||||
EKU_OIDS = {
|
||||
'server_auth': ExtendedKeyUsageOID.SERVER_AUTH,
|
||||
'client_auth': ExtendedKeyUsageOID.CLIENT_AUTH,
|
||||
'code_signing': ExtendedKeyUsageOID.CODE_SIGNING,
|
||||
'email_protection': ExtendedKeyUsageOID.EMAIL_PROTECTION,
|
||||
'time_stamping': ExtendedKeyUsageOID.TIME_STAMPING,
|
||||
'ocsp_signing': ExtendedKeyUsageOID.OCSP_SIGNING,
|
||||
'ipsec_ike': ExtendedKeyUsageOID.IPSEC_IKE,
|
||||
'ike_intermediate': x509.ObjectIdentifier('1.3.6.1.5.5.8.2.2'),
|
||||
}
|
||||
|
||||
#: The revocation reasons revoke takes.
|
||||
REASONS = ('unspecified', 'key_compromise', 'affiliation_changed', 'superseded', 'cessation_of_operation',
|
||||
'privilege_withdrawn')
|
||||
@@ -457,13 +445,19 @@ def general_names(values, allowed):
|
||||
return names
|
||||
|
||||
|
||||
def leaf_extensions(template, names):
|
||||
"""The extensions of a certificate of the template with the subject alternative names."""
|
||||
def leaf_extensions(cfg, template, names):
|
||||
"""The extensions of a certificate of the template with the subject alternative names: the extended key usages by
|
||||
their OIDs (vpnrouter.extended_key_usages), and the template's policies as certificate policies."""
|
||||
usage = x509.KeyUsage(**{option: option in template['key_usage'] for option in vpnrouter.KEY_USAGES},
|
||||
key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False)
|
||||
extensions = [(x509.BasicConstraints(ca=False, path_length=None), True), (usage, True)]
|
||||
if template['extended_key_usage']:
|
||||
extensions.append((x509.ExtendedKeyUsage([EKU_OIDS[item] for item in template['extended_key_usage']]), False))
|
||||
oids = vpnrouter.extended_key_usages(cfg)
|
||||
extensions.append((x509.ExtendedKeyUsage([x509.ObjectIdentifier(oids[item])
|
||||
for item in template['extended_key_usage']]), False))
|
||||
if template['policies']:
|
||||
extensions.append((x509.CertificatePolicies([x509.PolicyInformation(x509.ObjectIdentifier(oid), None)
|
||||
for oid in template['policies']]), False))
|
||||
if names:
|
||||
extensions.append((x509.SubjectAlternativeName(names), False))
|
||||
return extensions
|
||||
@@ -589,7 +583,7 @@ def ensure_router_cert(connection, cfg, ctx, name, urls):
|
||||
template = vpnrouter.pki_templates(cfg)[template_name]
|
||||
key = new_key(template['key'])
|
||||
cert = sign(subject, key.public_key(), issuer['cert'], issuer['key'], template['days'],
|
||||
leaf_extensions(template, general_names(sans, vpnrouter.SAN_TYPES)), urls, ca)
|
||||
leaf_extensions(cfg, template, general_names(sans, vpnrouter.SAN_TYPES)), urls, ca)
|
||||
record(connection, cert, ca, 'router', template_name, key)
|
||||
for dn, _, _ in current:
|
||||
drop_key(connection, dn)
|
||||
@@ -852,7 +846,7 @@ def issue(cfg, ca, template, csr=None, subject=None, sans=None, password=None):
|
||||
subject = check_subject(values['subject'], subject)
|
||||
names = general_names(([subject] if values['subject'] == 'dns' else []) + list(sans),
|
||||
set(values['sans']) | ({'dns'} if values['subject'] == 'dns' else set()))
|
||||
cert = sign(subject, public_key, issuer['cert'], issuer['key'], values['days'], leaf_extensions(values, names),
|
||||
cert = sign(subject, public_key, issuer['cert'], issuer['key'], values['days'], leaf_extensions(cfg, values, names),
|
||||
vpnrouter.pki_base_urls(cfg), ca)
|
||||
record(connection, cert, ca, 'external', template)
|
||||
root = authority(connection, 'root')['cert']
|
||||
@@ -880,7 +874,7 @@ def issue_user(cfg, name, email):
|
||||
issuer, values = issuer_of(connection, cfg, 'p2s', 'vpn-user')
|
||||
key = new_key(values['key'])
|
||||
cert = sign(name, key.public_key(), issuer['cert'], issuer['key'], values['days'],
|
||||
leaf_extensions(values, general_names([name, email], vpnrouter.SAN_TYPES)),
|
||||
leaf_extensions(cfg, values, general_names([name, email], vpnrouter.SAN_TYPES)),
|
||||
vpnrouter.pki_base_urls(cfg), 'p2s')
|
||||
for dn, _, _ in holders(connection, 'p2s', user_condition(name)):
|
||||
drop_key(connection, dn)
|
||||
|
||||
@@ -781,10 +781,11 @@ def backup(method, path, body):
|
||||
def ca(method, path, body):
|
||||
"""Answer a /ca request with vpnrouter_pki: the HTTP status and the JSON document.
|
||||
|
||||
GET /ca {"settings", "templates", "status", "certificates"}: the pki section,
|
||||
the templates with the built-in ones, the root, the issuing CAs, the
|
||||
retired CAs and the warnings (vpnrouter_pki.status), and the
|
||||
certificates issued, newest first
|
||||
GET /ca {"settings", "templates", "extended_key_usages", "status",
|
||||
"certificates"}: the pki section, the templates with the built-in
|
||||
ones, the extended key usages, each {oid, builtin}, the root, the
|
||||
issuing CAs, the retired CAs and the warnings (vpnrouter_pki.status),
|
||||
and the certificates issued, newest first
|
||||
PUT /ca change any of the settings of the pki section
|
||||
POST /ca/certificates issue a certificate: {"ca", "template", "csr"} answers {"serial",
|
||||
"certificate", "chain"}; {"ca", "template", "subject", "sans",
|
||||
@@ -811,6 +812,8 @@ def ca(method, path, body):
|
||||
return 200, vpnrouter_pki.bundle(route[2])
|
||||
if route == ('GET',):
|
||||
return 200, {'settings': cfg['pki'], 'templates': vpnrouter.pki_templates(cfg),
|
||||
'extended_key_usages': {name: {'oid': oid, 'builtin': name in vpnrouter.EXTENDED_KEY_USAGES}
|
||||
for name, oid in vpnrouter.extended_key_usages(cfg).items()},
|
||||
'status': vpnrouter_pki.status(cfg), 'certificates': vpnrouter_pki.certificates()}
|
||||
if route == ('PUT',):
|
||||
settings = {option: body[option] for option in vpnrouter.SCHEMA['pki'] if option in body}
|
||||
|
||||
Reference in new issue
Block a user