diff --git a/README.md b/README.md index 7ec6861..a5aeb42 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ must have the type shown below; an unknown section or setting is an error. | `pki` | `s2s_name` | string | `VPN Router Site-to-Site CA` | Common name of the site-to-site issuing CA | | `pki` | `issuing_cas` | object | | The general-purpose issuing CAs, each a name and `{name, templates, acme}` (see Certificates) | | `pki` | `templates` | object | | Certificate templates, each a name and the options it changes or defines (see Certificates) | +| `pki` | `extended_key_usages` | object | | Custom extended key usages templates may give, each a name and its OID in dotted form (see Certificates) | | `pki` | `root_key_warning` | boolean | `true` | `vpn-router-ca status` and `/ca` warn while the root key is on the router; `false` silences that warning | | `metrics` | `enabled` | boolean | `true` | The metrics service: collection, history and `/metrics` | | `metrics` | `listen` | string | `internal` | The interfaces `/metrics` is served on: `internal`, `external` or `both` | @@ -442,7 +443,8 @@ A template says what a certificate holds: | `key` | string | `rsa-3072` | `rsa-2048`, `rsa-3072`, `rsa-4096`, `ec-p256` or `ec-p384`, for a key the router makes | | `days` | integer | `365` | Days the certificate is valid, at most until its CA's certificate expires | | `key_usage` | list | `digital_signature` | Of `digital_signature`, `content_commitment`, `key_encipherment`, `data_encipherment`, `key_agreement` | -| `extended_key_usage` | list | | Of `server_auth`, `client_auth`, `code_signing`, `email_protection`, `time_stamping`, `ocsp_signing`, `ipsec_ike`, `ike_intermediate` | +| `extended_key_usage` | list | | Of the built-in `server_auth`, `client_auth`, `code_signing`, `email_protection`, `time_stamping`, `ocsp_signing`, `ipsec_ike`, `ike_intermediate`, and the custom usages of `pki.extended_key_usages` | +| `policies` | list | | Certificate policy OIDs in dotted form, written into a Certificate Policies extension | | `subject` | string | `name` | The common name: `dns`, a DNS name, which is also a subject alternative name; `name`, any name; `email`, an e-mail address | | `sans` | list | | The subject alternative names allowed: of `dns`, `ip`, `email` | @@ -461,15 +463,25 @@ templates of its own, named like the issuing CAs: ```json "pki": { - "issuing_cas": { "servers": { "name": "Example Server CA", "templates": ["server", "web"] } }, + "issuing_cas": { "servers": { "name": "Example Server CA", "templates": ["server", "web", "smartcard"] } }, + "extended_key_usages": { "smartcard_logon": "1.3.6.1.4.1.311.20.2.2" }, "templates": { "server": { "days": 397 }, "web": { "key": "ec-p256", "days": 90, "key_usage": ["digital_signature"], - "extended_key_usage": ["server_auth"], "subject": "dns", "sans": ["dns"] } + "extended_key_usage": ["server_auth"], "subject": "dns", "sans": ["dns"] }, + "smartcard": { "key_usage": ["digital_signature", "key_encipherment"], + "extended_key_usage": ["client_auth", "smartcard_logon"], "subject": "name", + "sans": ["email"], "policies": ["1.3.6.1.4.1.99999.1.1"] } } } ``` +A custom extended key usage is first added to `pki.extended_key_usages`, a name of at most 31 +lowercase letters, digits, hyphens and underscores that is not a built-in one, and its OID; a +template then gives it by its name. A usage a template gives cannot be removed. `policies` puts +the OIDs of the issuance policies a certificate is issued under into its Certificate Policies +extension, without qualifiers. + ### Issuing and revoking ```sh @@ -1072,7 +1084,7 @@ A change is made, checked and applied as for `/site-to-site`. | Request | Body | Answer | | --- | --- | --- | -| `GET /ca` | | `{"settings", "templates", "status", "certificates"}` | +| `GET /ca` | | `{"settings", "templates", "extended_key_usages", "status", "certificates"}` | | `PUT /ca` | any of the settings of the `pki` section | `{"settings", "templates"}` | | `POST /ca/certificates` | `{"ca", "template", "csr"}` | 201 and `{"serial", "certificate", "chain"}`, PEM | | `POST /ca/certificates` | `{"ca", "template", "subject", "sans", "password"}` | 201 and `{"serial", "pfx"}`, a PKCS#12 file in base64 | @@ -1086,6 +1098,7 @@ The answer of `GET /ca` holds: - **`settings`:** the `pki` section. - **`templates`:** every template, the built-in ones included, with all its options. +- **`extended_key_usages`:** every extended key usage templates may give, name -> `{"oid", "builtin"}`. - **`status`:** `root` (`subject`, `expires`, `key`, whether the root key is on the router, `crl_next_update` and `crl_source`, `signed` or `uploaded`), `authorities`, one per issuing CA (`name`, `common_name`, `state`: `active`, `pending` or `missing`, `builtin`, `templates`, `acme`, `subject`, `expires`, `key`, `crl_next_update`, `acme_url`), `retired` and `warnings`. - **`certificates`:** the certificates issued, newest first, each `{"serial", "ca", "kind", "template", "subject", "sans", "created", "expires", "revoked", "reason", "state"}`; `kind` is `ca`, `router`, `user` or `external`, and `state` `valid`, `expired` or `revoked`. diff --git a/debian-package/debian/changelog b/debian-package/debian/changelog index 2a4c4a9..1132798 100644 --- a/debian-package/debian/changelog +++ b/debian-package/debian/changelog @@ -1,3 +1,17 @@ +vpn-router (0.22.0-1) unstable; urgency=medium + + 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. + + * pki.extended_key_usages maps a name to the OID of a custom extended + key usage; a template's extended_key_usage takes the built-in and the + custom names. + * The template option policies lists certificate policy OIDs, written + into a Certificate Policies extension. + * GET /ca answers extended_key_usages, each usage with its OID and + whether it is built in. + + -- Slawomir Koszewski Mon, 02 Nov 2026 14:00:00 +0100 + vpn-router (0.21.5-1) unstable; urgency=medium fix: /status/full of the status web service gives an authenticated client the whole status, whatever web.status_content says. diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter.py b/debian-package/src/usr/lib/vpn-router/vpnrouter.py index c2ab265..0f7ecef 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter.py @@ -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.""" diff --git a/debian-package/src/usr/lib/vpn-router/vpnrouter_pki.py b/debian-package/src/usr/lib/vpn-router/vpnrouter_pki.py index 4bbcd30..622145f 100644 --- a/debian-package/src/usr/lib/vpn-router/vpnrouter_pki.py +++ b/debian-package/src/usr/lib/vpn-router/vpnrouter_pki.py @@ -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: -