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
@@ -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`.
|
||||
|
||||
|
||||
@@ -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 <slawek@koszewscy.waw.pl> 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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -92,6 +92,9 @@
|
||||
"days": 397
|
||||
}
|
||||
},
|
||||
"extended_key_usages": {
|
||||
"smartcard_logon": "1.3.6.1.4.1.311.20.2.2"
|
||||
},
|
||||
"root_key_warning": true
|
||||
},
|
||||
"metrics": {
|
||||
|
||||
Reference in new issue
Block a user