diff options
-rw-r--r-- | data/configd-include.json | 1 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/include/pki/ca-certificate.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/include/pki/certificate-key.xml.i | 23 | ||||
-rw-r--r-- | interface-definitions/include/pki/certificate.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/include/pki/dh-parameters.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/include/pki/openvpn_tls-auth.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/include/pki/private-key.xml.i | 30 | ||||
-rw-r--r-- | interface-definitions/include/pki/public-key.xml.i | 14 | ||||
-rw-r--r-- | interface-definitions/pki.xml.in | 203 | ||||
-rw-r--r-- | op-mode-definitions/pki.xml.in | 281 | ||||
-rw-r--r-- | python/vyos/pki.py | 305 | ||||
-rw-r--r-- | python/vyos/util.py | 19 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_pki.py | 95 | ||||
-rwxr-xr-x | src/conf_mode/pki.py | 167 | ||||
-rwxr-xr-x | src/op_mode/pki.py | 662 |
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><CA name></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><certificate name></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><certificate name></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><certificate name></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><DH name></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><key name></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><key name></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><key name></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><interface> <peer></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><peer></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) |