Skip to content

Commit

Permalink
pki: T5886: add support for ACME protocol (LetsEncrypt)
Browse files Browse the repository at this point in the history
The "idea" of this PR is to add new CLI nodes under the pki subsystem to
activate ACME for any given certificate.

vyos@vyos# set pki certificate NAME acme
Possible completions:
+  domain-name          Domain Name
   email                Email address to associate with certificate
   listen-address       Local IPv4 addresses to listen on
   rsa-key-size         Size of the RSA key (default: 2048)
   url                  Remote URL (default:
                        https://acme-v02.api.letsencrypt.org/directory)

Users choose if the CLI based custom certificates are used
  set pki certificate EXAMPLE acme certificate <base64>
or if it should be generated via ACME.

The ACME server URL defaults to LetsEncrypt but can be changed to their staging
API for testing to not get blacklisted.
  set pki certificate EXAMPLE acme url https://acme-staging-v02.api.letsencrypt.org/directory

Certificate retrieval has a certbot --dry-run stage in verify() to see if it
can be generated.

After successful generation, the certificate is stored in under
/config/auth/letsencrypt. Once a certificate is referenced in the CLI (e.g. set
interfaces ethernet eth0 eapol certificate EXAMPLE) we call
vyos.config.get_config_dict() which will (if with_pki=True is set) blend in the
base64 encoded certificate into the JSON data structure normally used when
using a certificate set by the CLI.

Using this "design" does not need any change to any other code referencing the
PKI system, as the base64 encoded certificate is already there.

certbot renewal will call the PKI python script to trigger dependency updates.
  • Loading branch information
c-po committed Jan 6, 2024
1 parent fb4b97b commit b8db1a9
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 53 deletions.
3 changes: 3 additions & 0 deletions debian/control
Expand Up @@ -146,6 +146,9 @@ Depends:
# For "protocols igmp-proxy"
igmpproxy,
# End "protocols igmp-proxy"
# For "pki"
certbot,
# End "pki"
# For "service console-server"
conserver-client,
conserver-server,
Expand Down
3 changes: 3 additions & 0 deletions interface-definitions/include/constraint/email.xml.i
@@ -0,0 +1,3 @@
<!-- include start from constraint/email.xml.i -->
<regex>[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}</regex>
<!-- include end -->
54 changes: 54 additions & 0 deletions interface-definitions/pki.xml.in
Expand Up @@ -81,6 +81,60 @@
<constraintErrorMessage>Certificate is not base64-encoded</constraintErrorMessage>
</properties>
</leafNode>
<node name="acme">
<properties>
<help>Automatic Certificate Management Environment (ACME) request</help>
</properties>
<children>
#include <include/url-http-https.xml.i>
<leafNode name="url">
<defaultValue>https://acme-v02.api.letsencrypt.org/directory</defaultValue>
</leafNode>
<leafNode name="domain-name">
<properties>
<help>Domain Name</help>
<constraint>
<validator name="fqdn"/>
</constraint>
<constraintErrorMessage>Invalid domain name (RFC 1123 section 2).\nMay only contain letters, numbers and .-_</constraintErrorMessage>
<multi/>
</properties>
</leafNode>
<leafNode name="email">
<properties>
<help>Email address to associate with certificate</help>
<constraint>
#include <include/constraint/email.xml.i>
</constraint>
</properties>
</leafNode>
#include <include/listen-address-ipv4-single.xml.i>
<leafNode name="rsa-key-size">
<properties>
<help>Size of the RSA key</help>
<completionHelp>
<list>2048 3072 4096</list>
</completionHelp>
<valueHelp>
<format>2048</format>
<description>RSA key length 2048 bit</description>
</valueHelp>
<valueHelp>
<format>3072</format>
<description>RSA key length 3072 bit</description>
</valueHelp>
<valueHelp>
<format>4096</format>
<description>RSA key length 4096 bit</description>
</valueHelp>
<constraint>
<regex>(2048|3072|4096)</regex>
</constraint>
</properties>
<defaultValue>2048</defaultValue>
</leafNode>
</children>
</node>
#include <include/generic-description.xml.i>
<node name="private">
<properties>
Expand Down
37 changes: 37 additions & 0 deletions python/vyos/config.py
Expand Up @@ -92,6 +92,38 @@ def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict:
dest = ConfigDict(dest)
return ext_dict_merge(src, dest)

def config_dict_mangle_acme(name, cli_dict):
"""
Load CLI PKI dictionary and if an ACME certificate is used, load it's content
and place it into the CLI dictionary as it would be a "regular" CLI PKI based
certificate with private key
"""
from vyos.base import ConfigError
from vyos.defaults import directories
from vyos.utils.file import read_file
from vyos.pki import encode_certificate
from vyos.pki import encode_private_key
from vyos.pki import load_certificate
from vyos.pki import load_private_key

try:
vyos_certbot_dir = directories['certbot']

if 'acme' in cli_dict:
tmp = read_file(f'{vyos_certbot_dir}/live/{name}/cert.pem')
tmp = load_certificate(tmp, wrap_tags=False)
cert_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1])

tmp = read_file(f'{vyos_certbot_dir}/live/{name}/privkey.pem')
tmp = load_private_key(tmp, wrap_tags=False)
key_base64 = "".join(encode_private_key(tmp).strip().split("\n")[1:-1])
# install ACME based PEM keys into "regular" CLI config keys
cli_dict.update({'certificate' : cert_base64, 'private' : {'key' : key_base64}})
except:
raise ConfigError(f'Unable to load ACME certificates for "{name}"!')

return cli_dict

class Config(object):
"""
The class of config access objects.
Expand Down Expand Up @@ -306,6 +338,11 @@ def get_config_dict(self, path=[], effective=False, key_mangling=None,
no_tag_node_value_mangle=True,
get_first_key=True)
if pki_dict:
if 'certificate' in pki_dict:
for certificate in pki_dict['certificate']:
pki_dict['certificate'][certificate] = config_dict_mangle_acme(
certificate, pki_dict['certificate'][certificate])

conf_dict['pki'] = pki_dict

# save optional args for a call to get_config_defaults
Expand Down

0 comments on commit b8db1a9

Please sign in to comment.