summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/configd-include.json1
-rw-r--r--debian/control1
-rw-r--r--interface-definitions/include/pki/ca-certificate.xml.i14
-rw-r--r--interface-definitions/include/pki/certificate-key.xml.i23
-rw-r--r--interface-definitions/include/pki/certificate.xml.i14
-rw-r--r--interface-definitions/include/pki/dh-parameters.xml.i14
-rw-r--r--interface-definitions/include/pki/openvpn_tls-auth.xml.i14
-rw-r--r--interface-definitions/include/pki/private-key.xml.i30
-rw-r--r--interface-definitions/include/pki/public-key.xml.i14
-rw-r--r--interface-definitions/pki.xml.in203
-rw-r--r--op-mode-definitions/pki.xml.in281
-rw-r--r--python/vyos/pki.py305
-rw-r--r--python/vyos/util.py19
-rwxr-xr-xsmoketest/scripts/cli/test_pki.py95
-rwxr-xr-xsrc/conf_mode/pki.py167
-rwxr-xr-xsrc/op_mode/pki.py662
16 files changed, 1857 insertions, 0 deletions
diff --git a/data/configd-include.json b/data/configd-include.json
index ee939decd..2e6226097 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -32,6 +32,7 @@
"nat.py",
"nat66.py",
"ntp.py",
+"pki.py",
"policy.py",
"policy-local-route.py",
"protocols_bfd.py",
diff --git a/debian/control b/debian/control
index 75e12ebf4..c0805804e 100644
--- a/debian/control
+++ b/debian/control
@@ -111,6 +111,7 @@ Depends:
python3,
python3-certbot-nginx,
python3-crypt | python3-pycryptodome,
+ python3-cryptography,
python3-flask,
python3-hurry.filesize,
python3-isc-dhcp-leases,
diff --git a/interface-definitions/include/pki/ca-certificate.xml.i b/interface-definitions/include/pki/ca-certificate.xml.i
new file mode 100644
index 000000000..14295a281
--- /dev/null
+++ b/interface-definitions/include/pki/ca-certificate.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/ca-certificate.xml.i -->
+<leafNode name="ca-certificate">
+ <properties>
+ <help>Certificate Authority in PKI configuration</help>
+ <valueHelp>
+ <format>CA name</format>
+ <description>Name of CA in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki ca</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/certificate-key.xml.i b/interface-definitions/include/pki/certificate-key.xml.i
new file mode 100644
index 000000000..b68f38442
--- /dev/null
+++ b/interface-definitions/include/pki/certificate-key.xml.i
@@ -0,0 +1,23 @@
+<!-- include start from pki/certificate-key.xml.i -->
+<leafNode name="certificate">
+ <properties>
+ <help>Certificate and private key in PKI configuration</help>
+ <valueHelp>
+ <format>cert name</format>
+ <description>Name of certificate in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki certificate</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<leafNode name="private-key-passphrase">
+ <properties>
+ <help>Private key passphrase</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Passphrase to decrypt the private key</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/certificate.xml.i b/interface-definitions/include/pki/certificate.xml.i
new file mode 100644
index 000000000..436aa90ba
--- /dev/null
+++ b/interface-definitions/include/pki/certificate.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/certificate.xml.i -->
+<leafNode name="certificate">
+ <properties>
+ <help>Certificate in PKI configuration</help>
+ <valueHelp>
+ <format>cert name</format>
+ <description>Name of certificate in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki certificate</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/dh-parameters.xml.i b/interface-definitions/include/pki/dh-parameters.xml.i
new file mode 100644
index 000000000..6e69528e7
--- /dev/null
+++ b/interface-definitions/include/pki/dh-parameters.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/dh-parameters.xml.i -->
+<leafNode name="dh-parameters">
+ <properties>
+ <help>Diffie-Hellman parameters in PKI configuration</help>
+ <valueHelp>
+ <format>DH name</format>
+ <description>Name of DH params in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki dh</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/openvpn_tls-auth.xml.i b/interface-definitions/include/pki/openvpn_tls-auth.xml.i
new file mode 100644
index 000000000..2b9a69653
--- /dev/null
+++ b/interface-definitions/include/pki/openvpn_tls-auth.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/openvpn_tls-auth.xml.i -->
+<leafNode name="auth-key">
+ <properties>
+ <help>Static key for tls-auth in PKI configuration</help>
+ <valueHelp>
+ <format>key name</format>
+ <description>Name of static key in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki openvpn tls-auth</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/private-key.xml.i b/interface-definitions/include/pki/private-key.xml.i
new file mode 100644
index 000000000..6099daa89
--- /dev/null
+++ b/interface-definitions/include/pki/private-key.xml.i
@@ -0,0 +1,30 @@
+<!-- include start from pki/private-key.xml.i -->
+<node name="private">
+ <properties>
+ <help>Private key</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Private key in PKI configuration</help>
+ <valueHelp>
+ <format>key name</format>
+ <description>Name of private key in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki key-pair</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="passphrase">
+ <properties>
+ <help>Private key passphrase</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Passphrase to decrypt the private key</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/pki/public-key.xml.i b/interface-definitions/include/pki/public-key.xml.i
new file mode 100644
index 000000000..dfc6979fd
--- /dev/null
+++ b/interface-definitions/include/pki/public-key.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/public-key.xml.i -->
+<leafNode name="public-key">
+ <properties>
+ <help>Public key in PKI configuration</help>
+ <valueHelp>
+ <format>key name</format>
+ <description>Name of public key in PKI configuration</description>
+ </valueHelp>
+ <completionHelp>
+ <path>pki key-pair</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in
new file mode 100644
index 000000000..e818ae438
--- /dev/null
+++ b/interface-definitions/pki.xml.in
@@ -0,0 +1,203 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="pki" owner="${vyos_conf_scripts_dir}/pki.py">
+ <properties>
+ <help>VyOS PKI configuration</help>
+ </properties>
+ <children>
+ <tagNode name="ca">
+ <properties>
+ <help>Certificate Authority</help>
+ </properties>
+ <children>
+ <leafNode name="certificate">
+ <properties>
+ <help>CA certificate in PEM format</help>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description</help>
+ </properties>
+ </leafNode>
+ <node name="private">
+ <properties>
+ <help>CA private key in PEM format</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>CA private key in PEM format</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password-protected">
+ <properties>
+ <help>CA private key is password protected</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="crl">
+ <properties>
+ <help>Certificate revocation list in PEM format</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="certificate">
+ <properties>
+ <help>Certificate</help>
+ </properties>
+ <children>
+ <leafNode name="certificate">
+ <properties>
+ <help>Certificate in PEM format</help>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Description</help>
+ </properties>
+ </leafNode>
+ <node name="private">
+ <properties>
+ <help>Certificate private key</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Certificate private key in PEM format</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password-protected">
+ <properties>
+ <help>Certificate private key is password protected</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="revoke">
+ <properties>
+ <help>If CA is present, this certificate will be included in generated CRLs</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="dh">
+ <properties>
+ <help>Diffie-Hellman parameters</help>
+ </properties>
+ <children>
+ <leafNode name="parameters">
+ <properties>
+ <help>DH parameters in PEM format</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="key-pair">
+ <properties>
+ <help>Public and private keys</help>
+ </properties>
+ <children>
+ <node name="public">
+ <properties>
+ <help>Public key</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Public key in PEM format</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="private">
+ <properties>
+ <help>Private key</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Private key in PEM format</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password-protected">
+ <properties>
+ <help>Private key is password protected</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <node name="openvpn">
+ <properties>
+ <help>OpenVPN keys</help>
+ </properties>
+ <children>
+ <tagNode name="tls-auth">
+ <properties>
+ <help>OpenVPN TLS auth key</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>OpenVPN TLS auth key data</help>
+ </properties>
+ </leafNode>
+ <leafNode name="version">
+ <properties>
+ <help>OpenVPN TLS auth key version</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="x509">
+ <properties>
+ <help>X509 Settings</help>
+ </properties>
+ <children>
+ <node name="default">
+ <properties>
+ <help>X509 Default Values</help>
+ </properties>
+ <children>
+ <leafNode name="country">
+ <properties>
+ <help>Default country</help>
+ </properties>
+ <defaultValue>GB</defaultValue>
+ </leafNode>
+ <leafNode name="state">
+ <properties>
+ <help>Default state</help>
+ </properties>
+ <defaultValue>Some-State</defaultValue>
+ </leafNode>
+ <leafNode name="locality">
+ <properties>
+ <help>Default locality</help>
+ </properties>
+ <defaultValue>Some-City</defaultValue>
+ </leafNode>
+ <leafNode name="organization">
+ <properties>
+ <help>Default organization</help>
+ </properties>
+ <defaultValue>VyOS</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in
new file mode 100644
index 000000000..0cea3db08
--- /dev/null
+++ b/op-mode-definitions/pki.xml.in
@@ -0,0 +1,281 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="pki">
+ <properties>
+ <help>Generate PKI certificates and keys</help>
+ </properties>
+ <children>
+ <node name="ca">
+ <properties>
+ <help>Generate CA certificate</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated certificate into running configuration</help>
+ <completionHelp>
+ <list>&lt;CA name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname"</command>
+ </node>
+ <node name="certificate">
+ <properties>
+ <help>Generate certificate request</help>
+ </properties>
+ <children>
+ <node name="self-signed">
+ <properties>
+ <help>Generate self-signed certificate</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated self-signed certificate into running configuration</help>
+ <completionHelp>
+ <list>&lt;certificate name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --self-sign</command>
+ </node>
+ <tagNode name="sign">
+ <properties>
+ <help>Sign generated certificate with specified CA certificate</help>
+ <completionHelp>
+ <path>pki ca</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated certificate into running configuration</help>
+ <completionHelp>
+ <list>&lt;certificate name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --sign "$5"</command>
+ </tagNode>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated certificate private key into running configuration</help>
+ <completionHelp>
+ <list>&lt;certificate name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname"</command>
+ </node>
+ <tagNode name="crl">
+ <properties>
+ <help>Generate CRL for specified CA certificate</help>
+ <completionHelp>
+ <path>pki ca</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="install">
+ <properties>
+ <help>Commands for installing generated CRL into running configuration</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --install</command>
+ </leafNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4"</command>
+ </tagNode>
+ <node name="dh">
+ <properties>
+ <help>Generate DH parameters</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated DH parameters into running configuration</help>
+ <completionHelp>
+ <list>&lt;DH name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "noname"</command>
+ </node>
+ <node name="key-pair">
+ <properties>
+ <help>Generate a key pair</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated key pair into running configuration</help>
+ <completionHelp>
+ <list>&lt;key name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "noname"</command>
+ </node>
+ <node name="openvpn">
+ <properties>
+ <help>Generate OpenVPN keys</help>
+ </properties>
+ <children>
+ <node name="tls-auth">
+ <properties>
+ <help>Generate OpenVPN TLS key</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated OpenVPN TLS key into running configuration</help>
+ <completionHelp>
+ <list>&lt;key name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "noname"</command>
+ </node>
+ </children>
+ </node>
+ <node name="ssh-key">
+ <properties>
+ <help>Generate SSH key</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated SSH key into running configuration</help>
+ <completionHelp>
+ <list>&lt;key name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "noname"</command>
+ </node>
+ <node name="wireguard">
+ <properties>
+ <help>Generate Wireguard keys</help>
+ </properties>
+ <children>
+ <node name="key-pair">
+ <properties>
+ <help>Generate Wireguard key pair for use with server or peer</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated Wireguard key into running configuration</help>
+ <completionHelp>
+ <list>&lt;interface&gt; &lt;peer&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "$6" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "noname"</command>
+ </node>
+ <node name="pre-shared-key">
+ <properties>
+ <help>Generate pre-shared key for use with a Wireguard peer</help>
+ </properties>
+ <children>
+ <tagNode name="install">
+ <properties>
+ <help>Commands for installing generated Wireguard psk on specified peer into running configuration</help>
+ <completionHelp>
+ <list>&lt;peer&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk "$6" --install</command>
+ </tagNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk "noname"</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="pki">
+ <properties>
+ <help>Show PKI certificates</help>
+ </properties>
+ <children>
+ <node name="ca">
+ <properties>
+ <help>Show CA certificates</help>
+ </properties>
+ <children>
+ <leafNode name="name">
+ <properties>
+ <help>Show CA certificate by name</help>
+ <completionHelp>
+ <path>pki ca</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "$5"</command>
+ </leafNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --ca "all"</command>
+ </node>
+ <node name="certificate">
+ <properties>
+ <help>Show certificates</help>
+ </properties>
+ <children>
+ <leafNode name="name">
+ <properties>
+ <help>Show certificate by name</help>
+ <completionHelp>
+ <path>pki certificate</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "$5"</command>
+ </leafNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --certificate "all"</command>
+ </node>
+ <node name="crl">
+ <properties>
+ <help>Show certificate revocation lists</help>
+ </properties>
+ <children>
+ <leafNode name="name">
+ <properties>
+ <help>Show certificate revocation lists from specified CA</help>
+ <completionHelp>
+ <path>pki ca</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --crl "$5"</command>
+ </leafNode>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show --crl "all"</command>
+ </node>
+ </children>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action show</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
new file mode 100644
index 000000000..80efe26b2
--- /dev/null
+++ b/python/vyos/pki.py
@@ -0,0 +1,305 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import datetime
+
+from cryptography import x509
+from cryptography.exceptions import InvalidSignature
+from cryptography.x509.extensions import ExtensionNotFound
+from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, ExtensionOID
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import dh
+from cryptography.hazmat.primitives.asymmetric import dsa
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+CERT_BEGIN='-----BEGIN CERTIFICATE-----\n'
+CERT_END='\n-----END CERTIFICATE-----'
+KEY_BEGIN='-----BEGIN PRIVATE KEY-----\n'
+KEY_END='\n-----END PRIVATE KEY-----'
+KEY_ENC_BEGIN='-----BEGIN ENCRYPTED PRIVATE KEY-----\n'
+KEY_ENC_END='\n-----END ENCRYPTED PRIVATE KEY-----'
+KEY_PUB_BEGIN='-----BEGIN PUBLIC KEY-----\n'
+KEY_PUB_END='\n-----END PUBLIC KEY-----'
+CRL_BEGIN='-----BEGIN X509 CRL-----\n'
+CRL_END='\n-----END X509 CRL-----'
+CSR_BEGIN='-----BEGIN CERTIFICATE REQUEST-----\n'
+CSR_END='\n-----END CERTIFICATE REQUEST-----'
+DH_BEGIN='-----BEGIN DH PARAMETERS-----\n'
+DH_END='\n-----END DH PARAMETERS-----'
+
+# Print functions
+
+encoding_map = {
+ 'PEM': serialization.Encoding.PEM,
+ 'OpenSSH': serialization.Encoding.OpenSSH
+}
+
+public_format_map = {
+ 'SubjectPublicKeyInfo': serialization.PublicFormat.SubjectPublicKeyInfo,
+ 'OpenSSH': serialization.PublicFormat.OpenSSH
+}
+
+private_format_map = {
+ 'PKCS8': serialization.PrivateFormat.PKCS8,
+ 'OpenSSH': serialization.PrivateFormat.OpenSSH
+}
+
+def encode_certificate(cert):
+ return cert.public_bytes(encoding=serialization.Encoding.PEM).decode('utf-8')
+
+def encode_public_key(cert, encoding='PEM', key_format='SubjectPublicKeyInfo'):
+ if encoding not in encoding_map:
+ encoding = 'PEM'
+ if key_format not in public_format_map:
+ key_format = 'SubjectPublicKeyInfo'
+ return cert.public_bytes(
+ encoding=encoding_map[encoding],
+ format=public_format_map[key_format]).decode('utf-8')
+
+def encode_private_key(private_key, encoding='PEM', key_format='PKCS8', passphrase=None):
+ if encoding not in encoding_map:
+ encoding = 'PEM'
+ if key_format not in private_format_map:
+ key_format = 'PKCS8'
+ encryption = serialization.NoEncryption() if not passphrase else serialization.BestAvailableEncryption(bytes(passphrase, 'utf-8'))
+ return private_key.private_bytes(
+ encoding=encoding_map[encoding],
+ format=private_format_map[key_format],
+ encryption_algorithm=encryption).decode('utf-8')
+
+def encode_dh_parameters(dh_parameters):
+ return dh_parameters.parameter_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.ParameterFormat.PKCS3).decode('utf-8')
+
+# EC Helper
+
+def get_elliptic_curve(size):
+ curve_func = None
+ name = f'SECP{size}R1'
+ if hasattr(ec, name):
+ curve_func = getattr(ec, name)
+ else:
+ curve_func = ec.SECP256R1() # Default to SECP256R1
+ return curve_func()
+
+# Creation functions
+
+def create_private_key(key_type, key_size=None):
+ private_key = None
+ if key_type == 'rsa':
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)
+ elif key_type == 'dsa':
+ private_key = dsa.generate_private_key(key_size=key_size)
+ elif key_type == 'ec':
+ curve = get_elliptic_curve(key_size)
+ private_key = ec.generate_private_key(curve)
+ return private_key
+
+def create_certificate_request(subject, private_key):
+ subject_obj = x509.Name([
+ x509.NameAttribute(NameOID.COUNTRY_NAME, subject['country']),
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, subject['state']),
+ x509.NameAttribute(NameOID.LOCALITY_NAME, subject['locality']),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject['organization']),
+ x509.NameAttribute(NameOID.COMMON_NAME, subject['common_name'])])
+
+ return x509.CertificateSigningRequestBuilder() \
+ .subject_name(subject_obj) \
+ .sign(private_key, hashes.SHA256())
+
+def create_certificate(cert_req, ca_cert, ca_private_key, valid_days=365, cert_type='server', is_ca=False):
+ ext_key_usage = []
+ if is_ca:
+ ext_key_usage = [ExtendedKeyUsageOID.CLIENT_AUTH, ExtendedKeyUsageOID.SERVER_AUTH]
+ elif cert_type == 'client':
+ ext_key_usage = [ExtendedKeyUsageOID.CLIENT_AUTH]
+ elif cert_type == 'server':
+ ext_key_usage = [ExtendedKeyUsageOID.SERVER_AUTH]
+
+ builder = x509.CertificateBuilder() \
+ .subject_name(cert_req.subject) \
+ .issuer_name(ca_cert.subject) \
+ .public_key(cert_req.public_key()) \
+ .serial_number(x509.random_serial_number()) \
+ .not_valid_before(datetime.datetime.utcnow()) \
+ .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=int(valid_days)))
+
+ builder = builder.add_extension(x509.BasicConstraints(ca=is_ca, path_length=None), critical=True)
+ builder = builder.add_extension(x509.ExtendedKeyUsage(ext_key_usage), critical=True)
+ builder = builder.add_extension(x509.KeyUsage(
+ digital_signature=True,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ key_cert_sign=is_ca,
+ crl_sign=is_ca,
+ encipher_only=False,
+ decipher_only=False), critical=True)
+
+ for ext in cert_req.extensions:
+ builder = builder.add_extension(ext, critical=False)
+
+ return builder.sign(ca_private_key, hashes.SHA256())
+
+def create_certificate_revocation_list(ca_cert, ca_private_key, serial_numbers=[]):
+ if not serial_numbers:
+ return False
+
+ builder = x509.CertificateRevocationListBuilder() \
+ .issuer_name(ca_cert.subject) \
+ .last_update(datetime.datetime.today()) \
+ .next_update(datetime.datetime.today() + datetime.timedelta(1, 0, 0))
+
+ for serial_number in serial_numbers:
+ revoked_cert = x509.RevokedCertificateBuilder() \
+ .serial_number(serial_number) \
+ .revocation_date(datetime.datetime.today()) \
+ .build()
+ builder = builder.add_revoked_certificate(revoked_cert)
+
+ return builder.sign(private_key=ca_private_key, algorithm=hashes.SHA256())
+
+def create_dh_parameters(bits=2048):
+ if not bits or bits < 512:
+ print("Invalid DH parameter key size")
+ return False
+
+ return dh.generate_parameters(generator=2, key_size=int(bits))
+
+# Wrap functions
+
+def wrap_public_key(raw_data):
+ return KEY_PUB_BEGIN + raw_data + KEY_PUB_END
+
+def wrap_private_key(raw_data, passphrase=None):
+ return (KEY_ENC_BEGIN if passphrase else KEY_BEGIN) + raw_data + (KEY_ENC_END if passphrase else KEY_END)
+
+def wrap_certificate_request(raw_data):
+ return CSR_BEGIN + raw_data + CSR_END
+
+def wrap_certificate(raw_data):
+ return CERT_BEGIN + raw_data + CERT_END
+
+def wrap_crl(raw_data):
+ return CRL_BEGIN + raw_data + CRL_END
+
+def wrap_dh_parameters(raw_data):
+ return DH_BEGIN + raw_data + DH_END
+
+# Load functions
+
+def load_public_key(raw_data, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_public_key(raw_data)
+
+ try:
+ return serialization.load_pem_public_key(bytes(raw_data, 'utf-8'))
+ except ValueError:
+ return False
+
+def load_private_key(raw_data, passphrase=None, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_private_key(raw_data, passphrase)
+
+ if passphrase:
+ passphrase = bytes(passphrase, 'utf-8')
+
+ try:
+ return serialization.load_pem_private_key(bytes(raw_data, 'utf-8'), password=passphrase)
+ except ValueError:
+ return False
+
+def load_certificate_request(raw_data, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_certificate_request(raw_data)
+
+ try:
+ return x509.load_pem_x509_csr(bytes(raw_data, 'utf-8'))
+ except ValueError:
+ return False
+
+def load_certificate(raw_data, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_certificate(raw_data)
+
+ try:
+ return x509.load_pem_x509_certificate(bytes(raw_data, 'utf-8'))
+ except ValueError:
+ return False
+
+def load_crl(raw_data, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_crl(raw_data)
+
+ try:
+ return x509.load_pem_x509_crl(bytes(raw_data, 'utf-8'))
+ except ValueError:
+ return False
+
+def load_dh_parameters(raw_data, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_dh_parameters(raw_data)
+
+ try:
+ return serialization.load_pem_parameters(bytes(raw_data, 'utf-8'))
+ except ValueError:
+ return False
+
+# Verify
+
+def is_ca_certificate(cert):
+ if not cert:
+ return False
+
+ try:
+ ext = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS)
+ return ext.value.ca
+ except ExtensionNotFound:
+ return False
+
+def verify_certificate(cert, ca_cert):
+ # Verify certificate was signed by specified CA
+ if ca_cert.subject != cert.issuer:
+ return False
+
+ ca_public_key = ca_cert.public_key()
+ try:
+ if isinstance(ca_public_key, rsa.RSAPublicKeyWithSerialization):
+ ca_public_key.verify(
+ cert.signature,
+ cert.tbs_certificate_bytes,
+ padding=padding.PKCS1v15(),
+ algorithm=cert.signature_hash_algorithm)
+ elif isinstance(ca_public_key, dsa.DSAPublicKeyWithSerialization):
+ ca_public_key.verify(
+ cert.signature,
+ cert.tbs_certificate_bytes,
+ algorithm=cert.signature_hash_algorithm)
+ elif isinstance(ca_public_key, ec.EllipticCurvePublicKeyWithSerialization):
+ ca_public_key.verify(
+ cert.signature,
+ cert.tbs_certificate_bytes,
+ signature_algorithm=ec.ECDSA(cert.signature_hash_algorithm))
+ else:
+ return False # We cannot verify it
+ return True
+ except InvalidSignature:
+ return False
diff --git a/python/vyos/util.py b/python/vyos/util.py
index c318d58de..c3bf481ea 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -566,6 +566,25 @@ def wait_for_commit_lock():
while commit_in_progress():
sleep(1)
+def ask_input(question, default='', numeric_only=False, valid_responses=[]):
+ question_out = question
+ if default:
+ question_out += f' (Default: {default})'
+ response = ''
+ while True:
+ response = input(question_out + ' ').strip()
+ if not response and default:
+ return default
+ if numeric_only:
+ if not response.isnumeric():
+ print("Invalid value, try again.")
+ continue
+ response = int(response)
+ if valid_responses and response not in valid_responses:
+ print("Invalid value, try again.")
+ continue
+ break
+ return response
def ask_yes_no(question, default=False) -> bool:
"""Ask a yes/no question via input() and return their answer."""
diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py
new file mode 100755
index 000000000..60287a0b4
--- /dev/null
+++ b/smoketest/scripts/cli/test_pki.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+
+base_path = ['pki']
+
+valid_ca_cert = 'MIIDgTCCAmmgAwIBAgIUeM0mATGs+sKF7ViBM6DEf9fQ19swDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHVnlPUyBDQTAeFw0yMTA2MjgxMzE2NDZaFw0yNjA2MjcxMzE2NDZaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxEDAOBgNVBAMMB1Z5T1MgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK98WwZIqgC6teHPSsyKLLRtboy55aisJN0D3iHJ8WGKkDmIrdCR2LI4J5C82ErfPOzl4Ck4vTmqh8wnuK/dhUxxzNdFJBMPHAe/E+UawYrubtJj5g8iHYowZJT5HQKnZbcqlPvl6EizA+etO48WGljKhpimj9/LVTp81+BtFNP4tJ/vOl+iqyJ0+PxiqQNDJgAF18meQRKaT9CcXycsciG9snMlB1tdOR7KDbi8lJ86lOi5ukPJaiMgWEu4UlyFVyHJ/68NvtwRhYerMoQquqDs21OXkOd8spZL6qEsxMeK8InedA7abPaxgxORpHguPQV4Ib5HBH9Chdb9zBMheZAgMBAAGjRTBDMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEAbwJZifMEDbrKPQfGLp7ZA1muM728o4EYmmE79eWwH22wGMSZI7T2xr5zRlFLs+Jha917yQK4b5xBMjQRAJlHKjzNLJ+3XaGlnWjaTBJ2SC5YktrmXRAIS7PxTRk/r1bHs/D00+sEWewbFYr8Js4a1Cv4TksTNyjHx8pvphA+KIx/4qdojTslz+oH/cakUz0M9fh2B2xsO4bab5vX+LGLCK7jjeAL4Zyjf1hDyx+Ri79L5N8h4Q69fER4cIkW7KVKUOyjEg3N4ST56urdycmyq9bXFz5pRxuZLInA6RRToJrL8i0aPLJ6SyMujfREfjqOxdW5vyNF5/RkY+5Nz8JMgQ=='
+valid_ca_private_key = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK98WwZIqgC6teHPSsyKLLRtboy55aisJN0D3iHJ8WGKkDmIrdCR2LI4J5C82ErfPOzl4Ck4vTmqh8wnuK/dhUxxzNdFJBMPHAe/E+UawYrubtJj5g8iHYowZJT5HQKnZbcqlPvl6EizA+etO48WGljKhpimj9/LVTp81+BtFNP4tJ/vOl+iqyJ0+PxiqQNDJgAF18meQRKaT9CcXycsciG9snMlB1tdOR7KDbi8lJ86lOi5ukPJaiMgWEu4UlyFVyHJ/68NvtwRhYerMoQquqDs21OXkOd8spZL6qEsxMeK8InedA7abPaxgxORpHguPQV4Ib5HBH9Chdb9zBMheZAgMBAAECggEAa/CK5L0DcAvkrd9OS9lDokFhJ1qqM1KZ9NHrJyW7gP/KWow0RUqEuKtAxuj8+jOcdn4PRuV6tiUIt5iiJQ/MjYF6ktTqrZq+5nPDnzXGBTZ2vuXYxKvgThqczD4RuJfsa8O1wR/nmit/k6q0kCVmnakJI1+laHWNZRjXUs+DXcWbrUN5D4/5kyjvFilH1c8arfrO2O4DcwfX1zNbxicgYrGmjE5m6WCZKWdcgpBcIQShZfNATfXIEZ16WmDIFZnuOEUtFAzweR2ataLQNoyaTUeEe6g+ZDtUQIGKR/f0+Z4T/JMJfPX/vRn0l3nRJWWC7Okpa2xb0hVdBmS/op+TNQKBgQDvNGAkS4uUx8xw724kzCKQJRnzR80AQ6b2FoqRbAevWm+i0ntsCMyvCItAQS8Bw+9fgITvsmd9SdYPncMQZ1oQYPk5yso/SPUyuNPXtygDxUP1xS1yja5MObqyrq2O2EzcxiVxEHGlZMLTNxNA1tE8nF4c0nQpV/EfLtkQFnnUSwKBgQDZOA2hiLaiDlPj03S4UXDu6aUD2o07782CUKl6A331ZhH/8zGEiUvBKg8IG/2FyCHQDC0C6rbfoarAhrRGbDHKkDTKNmThTj+IYBkLt/5OATvqkEw8eL0nB+PY5JKH04/jE0F/YM/StUsgxvMCVhtp0u/d2Hq4V9skxah6oFbtKwKBgGEvs3wroWtyffLIpMSYl9Ze7Js2aekYk4ZahDQvYzPwl3jc8b5kGN1oqEMT+MhL1j7EFb7ZikiSLkGsBGvuwd3zuG6toNxzhQP1qkRzqvNVO5ZoZV2siMt5jQw6AlQON7RfYSj92F6tgKaWMuFeJibtFSO6se12SIY134U0zIzfAoGAQWF7yNkrj4+cdICbKzdoNKEiyAwqYpYFV2oL+OvAJ/L3DAEZMHla0eNk7t3t6yyX8NUZXz1imeFBUf25mVDLk9rf6NWCe8ZfnR6/qyVQaA47CJkyOSlmVa8sR4ZVDIkDUCflmP98zkE/QbhgQJ3GVo3lIPMdzQq0rVbJJU/Jmk0CgYEAtHRNaoKBsxKfb7N7ewlaMzwcULIORODjWM8MUXM+R50F/2uYMiTvpz6eIUVfXoFyQoioYI8kcDZ8NamiQIS7uZsHfKpgMDJkV3kOoZQusoDhasGQ0SOnxbz/y0XmNUtAePipH0jPY1SYUvWbvm2ya4aWVhBFly9hi2ZeHiVxVhk='
+valid_cert = 'MIIB9zCCAZygAwIBAgIUQ5G1nyASL/YsKGyLNGhRPPQyo4kwCgYIKoZIzj0EAwIwXjELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEXMBUGA1UEAwwOVnlPUyBUZXN0IENlcnQwHhcNMjEwNjI4MTMyNjIyWhcNMjIwNjI4MTMyNjIyWjBeMQswCQYDVQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTESMBAGA1UEBwwJU29tZS1DaXR5MQ0wCwYDVQQKDARWeU9TMRcwFQYDVQQDDA5WeU9TIFRlc3QgQ2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBsebIt+8rr2UysTpL8NnYUtmt47e3sC3H9IO8iI/N4uFrmGVgTLE2G+RDGzZgG/r7LviJSTuE9HX7wHLcIr0SmjODA2MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0kAMEYCIQD5xK5kdC3TJ7SZrBGvzIM7E7Cil/KZJUyQDR9eFNNZVQIhALg8DTfrwAawf8L+Ncjn/l2gd5cB0nGij0D7uYnm3zf/'
+valid_dh_params = 'MIIBCAKCAQEAnNldZCrJk5MxhFoUlvvaYmUO+TmtL0uL62H2RIHJ+O0R+8vzdGPh6zDAzo46EJK735haUgu8+A1RTsXDOXcwBqDlVe0hYj9KaPHz1HpfNKntpoPCJAYJwiH8dd5zVMH+iBwEKlrfteV9vWHn0HUxgLJFSLp5o6y0qpKPREJu6k0XguGScrPaIw6RUwsoDy3unHfk+YeC0o040R18F75V1mXWTjQlEgM7ZO2JZkLGkhW30jB0vSHrkrFqOvtPUiyG7r3+j18IUYLTN0s+5FOCfCjvSVKibNlB1vUz5y/9Ve8roctpkRM/5R5FA0mtbl7U/yMSX4FRIQ/A9BlHiu4bowIBAg=='
+valid_public_ec_key = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAoInInwjlu/3+wDqvRa/Eyg3EMvBpPyq2v4jqEtEh2n4lOCi7ZgNjr+1sQSvrn8mccpALYl3/RKOougC5oQzCg=='
+valid_private_rsa_key = 'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDoAVyJPpcLBFs2NdS1qMOSj7mwKBKVZiBN3nqbLiOvEHbVe22UMNvUFU3sGs2Ta2zXwhPF3d6vPPsGlYTkO3XAffMSNXhjCsvWHiIOR4JrWf598Bpt+txBsxsa12kM3/HM7RDf3zdN2gTtwzrcWzu+zOTXlqJ2OSq/BRRZO9IMbQLQ1/h42GJHEr4THnY4zDqUjmMmIuiBXn4xoE4KFLH1+xPTVleeKvPPeJ1wsshoUjlXYOgcsrXasDUt5gtkkXsVQwR9Lvbh+RcBhT+tJmrX9Cwq4YAd3tLSNJARS9HanRZ8uV0RTyZsImdw1Fr5ySpG2oEp/Z5mbL6QYqDmQ+DAgMBAAECggEAGu7qMQf0TEJo98J3CtmwQ2Rnep+ksfdM8uVvbJ4hXs1+h7Mx8jr2XVoDEZLBgA17z8lSvIjvkz92mdgaZ8E5bbPAqSiSAeapf3A/0AmFIDH2scyxehyvVrVn6blygAvzGLr+o5hm2ZIqSySVq8jHBbQiKrT/5CCvgvcH2Rj7dMXdT5lL73tCRJZsgvFNlxyj4Omj9Lh7SjL+tIwEQaLFbvANXrZ/BPyw4OlK8daBNg9b5GvJSDitAVMgDEEApGYu1iNwMM4UJSQAC27eJdr+qJO6DDqktWOyWcyXrxJ9mDVKFNbb9QNQZDj7bFfm6rCuSdH9yYe3vly+SNJqtyCiwQKBgQDvemt/57KiwQffmoKR65NAZsQvmA4PtELYOV8NPeYH1BZN/EPmCc74iELJdQPFDYy903aRJEPGt7jfqprdPexLwt73P/XiUjPrsbqgJqfF/EMiczxAktyW3xBt2lIWU1MUUmO1ps+ZZEg8Ks4eK/3+FWqbwZ8drDBUT9BthUA0oQKBgQDRHxU6bu938PGweFJcIG6U21nsYaWiwCiTLXA5vWZ+UEqz81BUye6tIcCDgeku3HvC/0ycvrBM9F4AZCjnnEvrAJHKl6e4j+C4IpghGQvRvQ9ihDs9JIHnaoUC1i8dE3ISbbp1r7CN+J/HnAC2OeECMJuffXdnkVWaxRdxU+9towKBgCwFVeNyJO00DI126o+GPVA2U9Pn4JXUbgEvMqDNgw5nVx5Iw/ZyUSBwc85yexnq7rcqOv5dKzRJK2u6AbOvoVMf5DqRAFL1B2RJDGRKFscXIwQfKLE6DeCR6oQ3AKXn9TqkFn4axsiMnZapy6/SKGNfbnRpOCWNNGkbLtYjC3VhAoGAN0kOZapaaM0sOEk3DOAOHBB5j4KpNYOztmU23Cz0YcR8W2KiBCh2jxLzQFEiAp+LoJu59156YX3hNB1GqySo9XHrGTJKxwJSmJucuHNUqphe7t6igqGaLkH89CkHv5oaeEDGIMLX3FC0fSMDFSnsEJYlLl8PKDRF+2rLrcxQ6h0CgYAZllNu8a7tE6cM6QsCILQnNjuLuZRX8/KYWRqBJxatwZXCcMe2jti1HKTVVVCyYffOFa1QcAjCPknAmAz80l3eg6a75NnEXo0J6YLAOOxd8fD2/HidhbceCmTF+3msidIzCsBidBkgn6V5TXx2IyMSxGsJxVHfSKeooUQn6q76sg=='
+
+class TestPKI(VyOSUnitTestSHIM.TestCase):
+ def setUp(self):
+ self.cli_delete(base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_valid_pki(self):
+ # Valid CA
+ self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_ca_cert])
+ self.cli_set(base_path + ['ca', 'smoketest', 'private', 'key', valid_ca_private_key])
+ self.cli_set(base_path + ['ca', 'smoketest', 'private', 'type', 'rsa'])
+
+ # Valid cert
+ self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_cert])
+
+ # Valid DH
+ self.cli_set(base_path + ['dh', 'smoketest', 'parameters', valid_dh_params])
+
+ # Valid public key
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'public', 'key', valid_public_ec_key])
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'type', 'ec'])
+
+ # Valid private key
+ self.cli_set(base_path + ['key-pair', 'smoketest1', 'private', 'key', valid_private_rsa_key])
+ self.cli_set(base_path + ['key-pair', 'smoketest1', 'type', 'rsa'])
+
+ self.cli_commit()
+
+ def test_invalid_ca_valid_certificate(self):
+ self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_cert])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_invalid_certificate(self):
+ self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', 'invalidcertdata'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_invalid_public_key(self):
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'public', 'key', 'invalidkeydata'])
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'type', 'rsa'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_invalid_private_key(self):
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'private', 'key', 'invalidkeydata'])
+ self.cli_set(base_path + ['key-pair', 'smoketest', 'type', 'rsa'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_invalid_dh_parameters(self):
+ self.cli_set(base_path + ['dh', 'smoketest', 'parameters', 'thisisinvalid'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
new file mode 100755
index 000000000..ef1b57650
--- /dev/null
+++ b/src/conf_mode/pki.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.pki import is_ca_certificate
+from vyos.pki import load_certificate
+from vyos.pki import load_certificate_request
+from vyos.pki import load_public_key
+from vyos.pki import load_private_key
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.util import ask_input
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['pki']
+ if not conf.exists(base):
+ return None
+
+ pki = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ default_values = defaults(base)
+ pki = dict_merge(default_values, pki)
+ return pki
+
+def is_valid_certificate(raw_data):
+ # If it loads correctly we're good, or return False
+ return load_certificate(raw_data, wrap_tags=True)
+
+def is_valid_ca_certificate(raw_data):
+ # Check if this is a valid certificate with CA attributes
+ cert = load_certificate(raw_data, wrap_tags=True)
+ if not cert:
+ return False
+ return is_ca_certificate(cert)
+
+def is_valid_public_key(raw_data):
+ # If it loads correctly we're good, or return False
+ return load_public_key(raw_data, wrap_tags=True)
+
+def is_valid_private_key(raw_data, protected=False):
+ # If it loads correctly we're good, or return False
+ # With encrypted private keys, we always return true as we cannot ask for password to verify
+ if protected:
+ return True
+ return load_private_key(raw_data, passphrase=None, wrap_tags=True)
+
+def is_valid_crl(raw_data):
+ # If it loads correctly we're good, or return False
+ return load_crl(raw_data, wrap_tags=True)
+
+def is_valid_dh_parameters(raw_data):
+ # If it loads correctly we're good, or return False
+ return load_dh_parameters(raw_data, wrap_tags=True)
+
+def verify(pki):
+ if not pki:
+ return None
+
+ if 'ca' in pki:
+ for name, ca_conf in pki['ca'].items():
+ if 'certificate' in ca_conf:
+ if not is_valid_ca_certificate(ca_conf['certificate']):
+ raise ConfigError(f'Invalid certificate on CA certificate "{name}"')
+
+ if 'private' in ca_conf and 'key' in ca_conf['private']:
+ private = ca_conf['private']
+ protected = 'password_protected' in private
+
+ if not is_valid_private_key(private['key'], protected):
+ raise ConfigError(f'Invalid private key on CA certificate "{name}"')
+
+ if 'crl' in ca_conf:
+ ca_crls = ca_conf['crl']
+ if isinstance(ca_crls, str):
+ ca_crls = [ca_crls]
+
+ for crl in ca_crls:
+ if not is_valid_crl(crl):
+ raise ConfigError(f'Invalid CRL on CA certificate "{name}"')
+
+ if 'certificate' in pki:
+ for name, cert_conf in pki['certificate'].items():
+ if 'certificate' in cert_conf:
+ if not is_valid_certificate(cert_conf['certificate']):
+ raise ConfigError(f'Invalid certificate on certificate "{name}"')
+
+ if 'private' in cert_conf and 'key' in cert_conf['private']:
+ private = cert_conf['private']
+ protected = 'password_protected' in private
+
+ if not is_valid_private_key(private['key'], protected):
+ raise ConfigError(f'Invalid private key on certificate "{name}"')
+
+ if 'dh' in pki:
+ for name, dh_conf in pki['dh'].items():
+ if 'parameters' in dh_conf:
+ if not is_valid_dh_parameters(dh_conf['parameters']):
+ raise ConfigError(f'Invalid DH parameters on "{name}"')
+
+ if 'key_pair' in pki:
+ for name, key_conf in pki['key_pair'].items():
+ if 'public' in key_conf and 'key' in key_conf['public']:
+ if not is_valid_public_key(key_conf['public']['key']):
+ raise ConfigError(f'Invalid public key on key-pair "{name}"')
+
+ if 'private' in key_conf and 'key' in key_conf['private']:
+ private = key_conf['private']
+ protected = 'password_protected' in private
+ if not is_valid_private_key(private['key'], protected):
+ raise ConfigError(f'Invalid private key on key-pair "{name}"')
+
+ if 'x509' in pki:
+ if 'default' in pki['x509']:
+ default_values = pki['x509']['default']
+ if 'country' in default_values:
+ country = default_values['country']
+ if len(country) != 2 or not country.isalpha():
+ raise ConfigError(f'Invalid default country value. Value must be 2 alpha characters.')
+
+ return None
+
+def generate(pki):
+ if not pki:
+ return None
+
+ return None
+
+def apply(pki):
+ if not pki:
+ return None
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
new file mode 100755
index 000000000..321a5e60d
--- /dev/null
+++ b/src/op_mode/pki.py
@@ -0,0 +1,662 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+import re
+import sys
+import tabulate
+
+from cryptography import x509
+from cryptography.x509.oid import ExtendedKeyUsageOID
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.pki import encode_certificate, encode_public_key, encode_private_key, encode_dh_parameters
+from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list
+from vyos.pki import create_private_key
+from vyos.pki import create_dh_parameters
+from vyos.pki import load_certificate, load_certificate_request, load_private_key, load_crl
+from vyos.pki import verify_certificate
+from vyos.xml import defaults
+from vyos.util import ask_input, ask_yes_no
+from vyos.util import cmd
+
+CERT_REQ_END = '-----END CERTIFICATE REQUEST-----'
+
+# Helper Functions
+
+def get_default_values():
+ # Fetch default x509 values
+ conf = Config()
+ base = ['pki', 'x509', 'default']
+ x509_defaults = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ default_values = defaults(base)
+ return dict_merge(default_values, x509_defaults)
+
+def get_config_ca_certificate(name=None):
+ # Fetch ca certificates from config
+ conf = Config()
+ base = ['pki', 'ca']
+
+ if not conf.exists(base):
+ return False
+
+ if name:
+ base = base + [name]
+ if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']):
+ return False
+
+ return conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+def get_config_certificate(name=None):
+ # Get certificates from config
+ conf = Config()
+ base = ['pki', 'certificate']
+
+ if not conf.exists(base):
+ return False
+
+ if name:
+ base = base + [name]
+ if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']):
+ return False
+
+ return conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+def get_certificate_ca(cert, ca_certs):
+ # Find CA certificate for given certificate
+ for ca_name, ca_dict in ca_certs.items():
+ if 'certificate' not in ca_dict:
+ continue
+
+ ca_cert = load_certificate(ca_dict['certificate'])
+
+ if not ca_cert:
+ continue
+
+ if verify_certificate(cert, ca_cert):
+ return ca_name
+ return None
+
+def get_config_revoked_certificates():
+ # Fetch revoked certificates from config
+ conf = Config()
+ base = ['pki', 'certificate']
+ if not conf.exists(base):
+ return {}
+
+ certificates = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ return {cert: cert_dict for cert, cert_dict in certificates.items() if 'revoke' in cert_dict}
+
+def get_revoked_by_serial_numbers(serial_numbers=[]):
+ # Return serial numbers of revoked certificates
+ certs_out = []
+ certs = get_config_certificate()
+ if certs:
+ for cert_name, cert_dict in certs.items():
+ if 'certificate' not in cert_dict:
+ continue
+
+ cert = load_certificate(cert_dict['certificate'])
+ if cert.serial_number in serial_numbers:
+ certs_out.append(cert_name)
+ else:
+ certs_out.append(str(cert.serial_number)[0:10] + '...')
+ return certs_out
+
+def install_certificate(name, cert='', private_key=None, key_type=None, key_passphrase=None, is_ca=False):
+ # Show conf commands for installing certificate
+ prefix = 'ca' if is_ca else 'certificate'
+ print("Configure mode commands to install:")
+
+ if cert:
+ cert_pem = "".join(encode_certificate(cert).strip().split("\n")[1:-1])
+ print("set pki %s %s certificate '%s'" % (prefix, name, cert_pem))
+
+ if private_key:
+ key_pem = "".join(encode_private_key(private_key, passphrase=key_passphrase).strip().split("\n")[1:-1])
+ print("set pki %s %s private key '%s'" % (prefix, name, key_pem))
+ if key_passphrase:
+ print("set pki %s %s private password-protected" % (prefix, name))
+
+def install_crl(ca_name, crl):
+ # Show conf commands for installing crl
+ print("Configure mode commands to install CRL:")
+ crl_pem = "".join(encode_public_key(crl).strip().split("\n")[1:-1])
+ print("set pki ca %s crl '%s'" % (ca_name, crl_pem))
+
+def install_dh_parameters(name, params):
+ # Show conf commands for installing dh params
+ print("Configure mode commands to install DH parameters:")
+ dh_pem = "".join(encode_dh_parameters(params).strip().split("\n")[1:-1])
+ print("set pki dh %s parameters '%s'" % (name, dh_pem))
+
+def install_ssh_key(name, public_key, private_key, passphrase=None):
+ # Show conf commands for installing ssh key
+ key_openssh = encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')
+ username = os.getlogin()
+ type_key_split = key_openssh.split(" ")
+ print("Configure mode commands to install SSH key:")
+ print("set system login user %s authentication public-keys %s key '%s'" % (username, name, type_key_split[1]))
+ print("set system login user %s authentication public-keys %s type '%s'" % (username, name, type_key_split[0]))
+ print("")
+ print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
+
+def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None):
+ # Show conf commands for installing key-pair
+ print("Configure mode commands to install key pair:")
+
+ if public_key:
+ install_public_key = ask_yes_no('Do you want to install the public key?', default=True)
+ public_key_pem = encode_public_key(public_key)
+
+ if install_public_key:
+ install_public_pem = "".join(public_key_pem.strip().split("\n")[1:-1])
+ print("set pki key-pair %s public key '%s'" % (name, install_public_pem))
+ else:
+ print("Public key:")
+ print(public_key_pem)
+
+ if private_key:
+ install_private_key = ask_yes_no('Do you want to install the private key?', default=True)
+ private_key_pem = encode_private_key(private_key, passphrase=passphrase)
+
+ if install_private_key:
+ install_private_pem = "".join(private_key_pem.strip().split("\n")[1:-1])
+ print("set pki key-pair %s private key '%s'" % (name, install_private_pem))
+ if passphrase:
+ print("set pki key-pair %s private password-protected" % (name,))
+ else:
+ print("Private key:")
+ print(private_key_pem)
+
+def install_wireguard_key(name, private_key, public_key):
+ # Show conf commands for installing wireguard key pairs
+ is_interface = re.match(r'^wg[\d]+$', name)
+
+ print("Configure mode commands to install key:")
+ if is_interface:
+ print("set interfaces wireguard %s private-key '%s'" % (name, private_key))
+ print("")
+ print("Public key for use on peer configuration: " + public_key)
+ else:
+ print("set interfaces wireguard [INTERFACE] peer %s pubkey '%s'" % (name, public_key))
+ print("")
+ print("Private key for use on peer configuration: " + private_key)
+
+def install_wireguard_psk(name, psk):
+ # Show conf commands for installing wireguard psk
+ print("set interfaces wireguard [INTERFACE] peer %s preshared-key '%s'" % (name, psk))
+
+def ask_passphrase():
+ passphrase = None
+ print("Note: If you plan to use the generated key on this router, do not encrypt the private key.")
+ if ask_yes_no('Do you want to encrypt the private key with a passphrase?'):
+ passphrase = ask_input('Enter passphrase:')
+ return passphrase
+
+# Generation functions
+
+def generate_private_key():
+ key_type = ask_input('Enter private key type: [rsa, dsa, ec]', default='rsa', valid_responses=['rsa', 'dsa', 'ec'])
+
+ size_valid = []
+ size_default = 0
+
+ if key_type in ['rsa', 'dsa']:
+ size_default = 2048
+ size_valid = [512, 1024, 2048, 4096]
+ elif key_type == 'ec':
+ size_default = 256
+ size_valid = [224, 256, 384, 521]
+
+ size = ask_input('Enter private key bits:', default=size_default, numeric_only=True, valid_responses=size_valid)
+
+ return create_private_key(key_type, size), key_type
+
+def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False):
+ if not private_key:
+ private_key, key_type = generate_private_key()
+
+ default_values = get_default_values()
+ subject = {}
+ subject['country'] = ask_input('Enter country code:', default=default_values['country'])
+ subject['state'] = ask_input('Enter state:', default=default_values['state'])
+ subject['locality'] = ask_input('Enter locality:', default=default_values['locality'])
+ subject['organization'] = ask_input('Enter organization name:', default=default_values['organization'])
+ subject['common_name'] = ask_input('Enter common name:', default='vyos.io')
+
+ cert_req = create_certificate_request(subject, private_key)
+
+ if return_request:
+ return cert_req
+
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_certificate(cert_req))
+ print(encode_private_key(private_key, passphrase=passphrase))
+ return None
+
+ print("Certificate request:")
+ print(encode_public_key(cert_req) + "\n")
+ install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False):
+ valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True)
+ cert_type = None
+ if not is_ca:
+ cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server'])
+ return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca)
+
+def generate_ca_certificate(name, install=False):
+ private_key, key_type = generate_private_key()
+ cert_req = generate_certificate_request(private_key, key_type, return_request=True)
+ cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True)
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_certificate(cert))
+ print(encode_private_key(private_key, passphrase=passphrase))
+ return None
+
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+
+def generate_certificate_sign(name, ca_name, install=False):
+ ca_dict = get_config_ca_certificate(ca_name)
+
+ if not ca_dict:
+ print(f"CA certificate or private key for '{ca_name}' not found")
+ return None
+
+ ca_cert = load_certificate(ca_dict['certificate'])
+
+ if not ca_cert:
+ print("Failed to load CA certificate, aborting")
+ return None
+
+ ca_private = ca_dict['private']
+ ca_private_passphrase = None
+ if 'password_protected' in ca_private:
+ ca_private_passphrase = ask_input('Enter CA private key passphrase:')
+ ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase)
+
+ if not ca_private_key:
+ print("Failed to load CA private key, aborting")
+ return None
+
+ private_key = None
+ key_type = None
+
+ cert_req = None
+ if not ask_yes_no('Do you already have a certificate request?'):
+ private_key, key_type = generate_private_key()
+ cert_req = generate_certificate_request(private_key, key_type, return_request=True)
+ else:
+ print("Paste certificate request and press enter:")
+ lines = []
+ curr_line = ''
+ while True:
+ curr_line = input().strip()
+ if not curr_line or curr_line == CERT_REQ_END:
+ break
+ lines.append(curr_line)
+
+ if not lines:
+ print("Aborted")
+ return None
+
+ wrap = lines[0].find('-----') < 0 # Only base64 pasted, add the CSR tags for parsing
+ cert_req = load_certificate_request("\n".join(lines), wrap)
+
+ if not cert_req:
+ print("Invalid certificate request")
+ return None
+
+ cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False)
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_certificate(cert))
+ print(encode_private_key(private_key, passphrase=passphrase))
+ return None
+
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False)
+
+def generate_certificate_selfsign(name, install=False):
+ private_key, key_type = generate_private_key()
+ cert_req = generate_certificate_request(private_key, key_type, return_request=True)
+ cert = generate_certificate(cert_req, cert_req, private_key, is_ca=False)
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_certificate(cert))
+ print(encode_private_key(private_key, passphrase=passphrase))
+ return None
+
+ install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+def generate_certificate_revocation_list(ca_name, install=False):
+ ca_dict = get_config_ca_certificate(ca_name)
+
+ if not ca_dict:
+ print(f"CA certificate or private key for '{ca_name}' not found")
+ return None
+
+ ca_cert = load_certificate(ca_dict['certificate'])
+
+ if not ca_cert:
+ print("Failed to load CA certificate, aborting")
+ return None
+
+ ca_private = ca_dict['private']
+ ca_private_passphrase = None
+ if 'password_protected' in ca_private:
+ ca_private_passphrase = ask_input('Enter CA private key passphrase:')
+ ca_private_key = load_private_key(ca_private['key'], passphrase=ca_private_passphrase)
+
+ if not ca_private_key:
+ print("Failed to load CA private key, aborting")
+ return None
+
+ revoked_certs = get_config_revoked_Certificates()
+ to_revoke = []
+
+ for cert_name, cert_dict in revoked_certs.items():
+ if 'certificate' not in cert_dict:
+ continue
+
+ cert_data = cert_dict['certificate']
+
+ try:
+ cert = load_certificate(cert_data)
+
+ if cert.issuer == ca_cert.subject:
+ to_revoke.append(cert.serial_number)
+ except ValueError:
+ continue
+
+ if not to_revoke:
+ print("No revoked certificates to add to the CRL")
+ return None
+
+ crl = create_certificate_revocation_list(ca_cert, ca_private_key, to_revoke)
+
+ if not crl:
+ print("Failed to create CRL")
+ return None
+
+ if not install:
+ print(encode_certificate(crl))
+ return None
+
+ install_crl(ca_name, crl)
+
+def generate_ssh_keypair(name, install=False):
+ private_key, key_type = generate_private_key()
+ public_key = private_key.public_key()
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'))
+ print("")
+ print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
+ return None
+
+ install_ssh_key(name, public_key, private_key, passphrase)
+
+def generate_dh_parameters(name, install=False):
+ bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True)
+
+ print("Generating parameters...")
+
+ dh_params = create_dh_parameters(bits)
+ if not dh_params:
+ print("Failed to create DH parameters")
+ return None
+
+ if not install:
+ print("DH Parameters:")
+ print(encode_dh_parameters(dh_params))
+
+ install_dh_parameters(name, dh_params)
+
+def generate_keypair(name, install=False):
+ private_key, key_type = generate_private_key()
+ public_key = private_key.public_key()
+ passphrase = ask_passphrase()
+
+ if not install:
+ print(encode_public_key(public_key))
+ print("")
+ print(encode_private_key(private_key, passphrase=passphrase))
+ return None
+
+ install_keypair(name, key_type, private_key, public_key, passphrase)
+
+def generate_openvpn_key(name, install=False):
+ result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"')
+
+ if not result:
+ print("Failed to generate OpenVPN key")
+ return None
+
+ if not install:
+ print(result)
+ return None
+
+ key_lines = result.split("\n")
+ key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
+ key_version = '1'
+
+ version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
+ if version_search:
+ key_version = version_search[1]
+
+ print("Configure mode commands to install OpenVPN key:")
+ print("set pki openvpn tls-auth %s key '%s'" % (name, key_data))
+ print("set pki openvpn tls-auth %s version '%s'" % (name, key_version))
+
+def generate_wireguard_key(name, install=False):
+ private_key = cmd('wg genkey')
+ public_key = cmd('wg pubkey', input=private_key)
+
+ if not install:
+ print("Private key: " + private_key)
+ print("Public key: " + public_key)
+ return None
+
+ install_wireguard_key(name, private_key, public_key)
+
+def generate_wireguard_psk(name, install=False):
+ psk = cmd('wg genpsk')
+
+ if not install:
+ print("Pre-shared key:")
+ print(psk)
+ return None
+
+ install_wireguard_psk(name, psk)
+
+# Show functions
+
+def show_certificate_authority(name=None):
+ headers = ['Name', 'Subject', 'Issued', 'Expiry', 'Private Key']
+ data = []
+ certs = get_config_ca_certificate()
+ if certs:
+ for cert_name, cert_dict in certs.items():
+ if name and name != cert_name:
+ continue
+ if 'certificate' not in cert_dict:
+ continue
+
+ cert = load_certificate(cert_dict['certificate'])
+
+ if not cert:
+ continue
+
+ have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No'
+ data.append([cert_name, cert.subject.rfc4514_string(), cert.not_valid_before, cert.not_valid_after, have_private])
+
+ print("Certificate Authorities:")
+ print(tabulate.tabulate(data, headers))
+
+def show_certificate(name=None):
+ headers = ['Name', 'Type', 'Subject CN', 'Issuer CN', 'Issued', 'Expiry', 'Revoked', 'Private Key', 'CA Present']
+ data = []
+ certs = get_config_certificate()
+ if certs:
+ ca_certs = get_config_ca_certificate()
+
+ for cert_name, cert_dict in certs.items():
+ if name and name != cert_name:
+ continue
+ if 'certificate' not in cert_dict:
+ continue
+
+ cert = load_certificate(cert_dict['certificate'])
+
+ if not cert:
+ continue
+
+ ca_name = get_certificate_ca(cert, ca_certs)
+ cert_subject_cn = cert.subject.rfc4514_string().split(",")[0]
+ cert_issuer_cn = cert.issuer.rfc4514_string().split(",")[0]
+ cert_type = 'Unknown'
+ ext = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
+ if ext and ExtendedKeyUsageOID.SERVER_AUTH in ext.value:
+ cert_type = 'Server'
+ elif ext and ExtendedKeyUsageOID.CLIENT_AUTH in ext.value:
+ cert_type = 'Client'
+
+ revoked = 'Yes' if 'revoke' in cert_dict else 'No'
+ have_private = 'Yes' if 'private' in cert_dict and 'key' in cert_dict['private'] else 'No'
+ have_ca = f'Yes ({ca_name})' if ca_name else 'No'
+ data.append([
+ cert_name, cert_type, cert_subject_cn, cert_issuer_cn,
+ cert.not_valid_before, cert.not_valid_after,
+ revoked, have_private, have_ca])
+
+ print("Certificates:")
+ print(tabulate.tabulate(data, headers))
+
+def show_crl(name=None):
+ headers = ['CA Name', 'Updated', 'Revokes']
+ data = []
+ certs = get_config_ca_certificate()
+ if certs:
+ for cert_name, cert_dict in certs.items():
+ if name and name != cert_name:
+ continue
+ if 'crl' not in cert_dict:
+ continue
+
+ crls = cert_dict['crl']
+ if isinstance(crls, str):
+ crls = [crls]
+
+ for crl_data in cert_dict['crl']:
+ crl = load_crl(crl_data)
+
+ if not crl:
+ continue
+
+ certs = get_revoked_by_serial_numbers([revoked.serial_number for revoked in crl])
+ data.append([cert_name, crl.last_update, ", ".join(certs)])
+
+ print("Certificate Revocation Lists:")
+ print(tabulate.tabulate(data, headers))
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='PKI action', required=True)
+
+ # X509
+ parser.add_argument('--ca', help='Certificate Authority', required=False)
+ parser.add_argument('--certificate', help='Certificate', required=False)
+ parser.add_argument('--crl', help='Certificate Revocation List', required=False)
+ parser.add_argument('--sign', help='Sign certificate with specified CA', required=False)
+ parser.add_argument('--self-sign', help='Self-sign the certificate', action='store_true')
+
+ # SSH
+ parser.add_argument('--ssh', help='SSH Key', required=False)
+
+ # DH
+ parser.add_argument('--dh', help='DH Parameters', required=False)
+
+ # Key pair
+ parser.add_argument('--keypair', help='Key pair', required=False)
+
+ # OpenVPN
+ parser.add_argument('--openvpn', help='OpenVPN TLS key', required=False)
+
+ # Wireguard
+ parser.add_argument('--wireguard', help='Wireguard', action='store_true')
+ parser.add_argument('--key', help='Wireguard key pair', required=False)
+ parser.add_argument('--psk', help='Wireguard pre shared key', required=False)
+
+ # Global
+ parser.add_argument('--install', help='Install generated keys into running-config', action='store_true')
+
+ args = parser.parse_args()
+
+ try:
+ if args.action == 'generate':
+ if args.ca:
+ generate_ca_certificate(args.ca, args.install)
+ elif args.certificate:
+ if args.sign:
+ generate_certificate_sign(args.certificate, args.sign, args.install)
+ elif args.self_sign:
+ generate_certificate_selfsign(args.certificate, args.install)
+ else:
+ generate_certificate_request(name=args.certificate, install=args.install)
+ elif args.crl:
+ generate_certificate_revocation_list(args.crl, args.install)
+ elif args.ssh:
+ generate_ssh_keypair(args.ssh, args.install)
+ elif args.dh:
+ generate_dh_parameters(args.dh, args.install)
+ elif args.keypair:
+ generate_keypair(args.keypair, args.install)
+ elif args.openvpn:
+ generate_openvpn_key(args.openvpn, args.install)
+ elif args.wireguard:
+ if args.key:
+ generate_wireguard_key(args.key, args.install)
+ elif args.psk:
+ generate_wireguard_psk(args.psk, args.install)
+ elif args.action == 'show':
+ if args.ca:
+ show_certificate_authority(None if args.ca == 'all' else args.ca)
+ elif args.certificate:
+ show_certificate(None if args.certificate == 'all' else args.certificate)
+ elif args.crl:
+ show_crl(None if args.crl == 'all' else args.crl)
+ else:
+ show_certificate_authority()
+ show_certificate()
+ show_crl()
+ except KeyboardInterrupt:
+ print("Aborted")
+ sys.exit(0)