Initial commit
This commit is contained in:
commit
6c02075a57
5 changed files with 574 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.python-version
|
||||
__pycache__/
|
25
models/config.py
Normal file
25
models/config.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
class config:
|
||||
@classmethod
|
||||
def __init__(self):
|
||||
for config_path in (
|
||||
os.path.expanduser("~/.config/step-ca-inspector"),
|
||||
os.environ.get("STEP_CA_INSPECTOR_CONF"),
|
||||
):
|
||||
if config_path is None:
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(config_path, "config.yaml")) as ymlfile:
|
||||
cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)
|
||||
break
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
print("No configuration file found")
|
||||
sys.exit(1)
|
||||
|
||||
for k, v in cfg.items():
|
||||
setattr(self, k, v)
|
156
models/ssh_cert.py
Normal file
156
models/ssh_cert.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
import base64
|
||||
import dateutil
|
||||
import json
|
||||
import mariadb
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import asymmetric, hashes, serialization
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from models.config import config
|
||||
from struct import unpack
|
||||
|
||||
|
||||
config()
|
||||
conn = mariadb.connect(
|
||||
host=config.database_host,
|
||||
user=config.database_user,
|
||||
password=config.database_password,
|
||||
database=config.database_name,
|
||||
)
|
||||
|
||||
|
||||
class list:
|
||||
certs = []
|
||||
|
||||
def __new__(cls, sort_key=None):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nkey FROM ssh_certs")
|
||||
|
||||
for (cert_serial,) in cur:
|
||||
cert_object = cert(cert_serial)
|
||||
cls.certs.append(cert_object)
|
||||
|
||||
cur.close()
|
||||
|
||||
if sort_key is not None:
|
||||
cls.certs.sort(key=lambda item: getattr(item, sort_key))
|
||||
|
||||
return cls.certs
|
||||
|
||||
|
||||
class cert:
|
||||
def __init__(self, serial):
|
||||
cert_raw = self.get_cert(serial)
|
||||
size = unpack(">I", cert_raw[:4])[0] + 4
|
||||
alg = cert_raw[4:size]
|
||||
|
||||
cert_pub_id = b" ".join([alg, base64.b64encode(cert_raw)])
|
||||
cert_revoked = self.get_cert_revoked(serial)
|
||||
self.load(cert_pub_id, cert_revoked, alg)
|
||||
|
||||
def load(self, cert_pub_id, cert_revoked, cert_alg):
|
||||
cert = serialization.load_ssh_public_identity(cert_pub_id)
|
||||
self.serial = cert.serial
|
||||
self.alg = cert_alg
|
||||
if cert.type == serialization.SSHCertificateType.USER:
|
||||
self.type = "User"
|
||||
self.key_id = cert.key_id
|
||||
self.principals = cert.valid_principals
|
||||
self.not_after = datetime.fromtimestamp(cert.valid_before).replace(
|
||||
tzinfo=timezone(offset=timedelta()), microsecond=0
|
||||
)
|
||||
self.not_before = datetime.fromtimestamp(cert.valid_after).replace(
|
||||
tzinfo=timezone(offset=timedelta()), microsecond=0
|
||||
)
|
||||
# TODO: Implement critical options parsing
|
||||
# cert.critical_options
|
||||
self.extensions = cert.extensions
|
||||
|
||||
(self.signing_key, self.signing_key_type, self.signing_key_hash) = (
|
||||
self.get_public_key_params(cert.signature_key())
|
||||
)
|
||||
|
||||
(self.public_key, self.public_key_type, self.public_key_hash) = (
|
||||
self.get_public_key_params(cert.public_key())
|
||||
)
|
||||
|
||||
self.public_identity = cert.public_bytes()
|
||||
|
||||
if cert_revoked is not None:
|
||||
self.revoked_at = dateutil.parser.isoparse(
|
||||
cert_revoked.get("RevokedAt")
|
||||
).replace(microsecond=0)
|
||||
else:
|
||||
self.revoked_at = None
|
||||
|
||||
now_with_tz = datetime.utcnow().replace(
|
||||
tzinfo=timezone(offset=timedelta()), microsecond=0
|
||||
)
|
||||
|
||||
if self.revoked_at is not None and self.revoked_at < now_with_tz:
|
||||
self.status = status(status.REVOKED)
|
||||
elif self.not_after < now_with_tz:
|
||||
self.status = status(status.EXPIRED)
|
||||
else:
|
||||
self.status = status(status.VALID)
|
||||
|
||||
def get_cert(self, cert_serial):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nvalue FROM ssh_certs WHERE nkey=?", (cert_serial,))
|
||||
if cur.rowcount > 0:
|
||||
(cert,) = cur.fetchone()
|
||||
else:
|
||||
cert = None
|
||||
|
||||
cur.close()
|
||||
return cert
|
||||
|
||||
def get_cert_revoked(self, cert_serial):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nvalue FROM revoked_ssh_certs WHERE nkey=?", (cert_serial,))
|
||||
if cur.rowcount > 0:
|
||||
(cert_revoked_raw,) = cur.fetchone()
|
||||
cert_revoked = json.loads(cert_revoked_raw)
|
||||
else:
|
||||
cert_revoked = None
|
||||
|
||||
cur.close()
|
||||
return cert_revoked
|
||||
|
||||
def get_public_key_params(self, public_key):
|
||||
if isinstance(public_key, asymmetric.ec.EllipticCurvePublicKey):
|
||||
key_type = "ECDSA"
|
||||
elif isinstance(public_key, asymmetric.ed25519.Ed25519PublicKey):
|
||||
key_type = "ED25519"
|
||||
elif isinstance(public_key, asymmetric.rsa.RSAPublicKey):
|
||||
key_type = "RSA"
|
||||
|
||||
key_str = public_key.public_bytes(
|
||||
serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH
|
||||
)
|
||||
|
||||
key_data = key_str.strip().split()[1]
|
||||
digest = hashes.Hash(hashes.SHA256())
|
||||
digest.update(base64.b64decode(key_data))
|
||||
hash_sha256 = digest.finalize()
|
||||
key_hash = base64.b64encode(hash_sha256)
|
||||
|
||||
return key_str, key_type, key_hash
|
||||
|
||||
|
||||
class status:
|
||||
REVOKED = 1
|
||||
EXPIRED = 2
|
||||
VALID = 3
|
||||
|
||||
def __init__(self, status):
|
||||
self.value = status
|
||||
|
||||
def __str__(self):
|
||||
if self.value == self.EXPIRED:
|
||||
return "Expired"
|
||||
elif self.value == self.REVOKED:
|
||||
return "Revoked"
|
||||
elif self.value == self.VALID:
|
||||
return "Valid"
|
||||
else:
|
||||
return "Undefined"
|
137
models/x509_cert.py
Normal file
137
models/x509_cert.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
import binascii
|
||||
import dateutil
|
||||
import json
|
||||
import mariadb
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from models.config import config
|
||||
|
||||
|
||||
config()
|
||||
conn = mariadb.connect(
|
||||
host = config.database_host,
|
||||
user = config.database_user,
|
||||
password = config.database_password,
|
||||
database = config.database_name
|
||||
)
|
||||
|
||||
|
||||
class list:
|
||||
certs = []
|
||||
|
||||
def __new__(cls, sort_key=None):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nkey FROM x509_certs")
|
||||
|
||||
for (cert_serial,) in cur:
|
||||
cert_object = cert(cert_serial)
|
||||
cls.certs.append(cert_object)
|
||||
|
||||
cur.close()
|
||||
|
||||
if sort_key is not None:
|
||||
cls.certs.sort(key=lambda item: getattr(item, sort_key))
|
||||
|
||||
return cls.certs
|
||||
|
||||
|
||||
|
||||
class cert:
|
||||
def __init__(self, serial):
|
||||
cert_der = self.get_cert(serial)
|
||||
cert_data = self.get_cert_data(serial)
|
||||
cert_revoked = self.get_cert_revoked(serial)
|
||||
self.load(cert_der, cert_data, cert_revoked)
|
||||
|
||||
|
||||
def load(self, cert_der, cert_data, cert_revoked):
|
||||
cert = x509.load_der_x509_certificate(cert_der)
|
||||
|
||||
self.pem = cert.public_bytes(serialization.Encoding.PEM)
|
||||
self.serial = str(cert.serial_number)
|
||||
self.sha256 = binascii.b2a_hex(cert.fingerprint(hashes.SHA256()))
|
||||
self.sha1 = binascii.b2a_hex(cert.fingerprint(hashes.SHA1()))
|
||||
self.md5 = binascii.b2a_hex(cert.fingerprint(hashes.MD5()))
|
||||
self.pub_alg = cert.public_key_algorithm_oid._name
|
||||
self.sig_alg = cert.signature_algorithm_oid._name
|
||||
self.issuer = cert.issuer.rfc4514_string()
|
||||
self.subject = cert.subject.rfc4514_string({x509.NameOID.EMAIL_ADDRESS: "E"})
|
||||
self.not_before = cert.not_valid_before_utc.replace(microsecond=0)
|
||||
self.not_after = cert.not_valid_after_utc.replace(microsecond=0)
|
||||
san_data = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
self.san_names = san_data.value.get_values_for_type(x509.GeneralName)
|
||||
self.provisioner = cert_data.get("provisioner", None)
|
||||
|
||||
if cert_revoked is not None:
|
||||
self.revoked_at = dateutil.parser.isoparse(
|
||||
cert_revoked.get("RevokedAt")
|
||||
).replace(microsecond=0)
|
||||
else:
|
||||
self.revoked_at = None
|
||||
|
||||
now_with_tz = datetime.utcnow().replace(
|
||||
tzinfo=timezone(offset=timedelta()), microsecond=0
|
||||
)
|
||||
|
||||
if self.revoked_at is not None and self.revoked_at < now_with_tz:
|
||||
self.status = status(status.REVOKED)
|
||||
elif self.not_after < now_with_tz:
|
||||
self.status = status(status.EXPIRED)
|
||||
else:
|
||||
self.status = status(status.VALID)
|
||||
|
||||
|
||||
def get_cert(self, cert_serial):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nvalue FROM x509_certs WHERE nkey=?", (cert_serial,))
|
||||
if cur.rowcount > 0:
|
||||
(cert,) = cur.fetchone()
|
||||
else:
|
||||
cert = None
|
||||
|
||||
cur.close()
|
||||
return cert
|
||||
|
||||
|
||||
def get_cert_data(self, cert_serial):
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT nvalue FROM x509_certs_data WHERE nkey=?", (cert_serial,))
|
||||
(cert_data_raw,) = cur.fetchone()
|
||||
cur.close()
|
||||
cert_data = json.loads(cert_data_raw)
|
||||
return cert_data
|
||||
|
||||
|
||||
def get_cert_revoked(self, cert_serial):
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT nvalue FROM revoked_x509_certs WHERE nkey=?", (cert_serial,)
|
||||
)
|
||||
if cur.rowcount > 0:
|
||||
(cert_revoked_raw,) = cur.fetchone()
|
||||
cert_revoked = json.loads(cert_revoked_raw)
|
||||
else:
|
||||
cert_revoked = None
|
||||
|
||||
cur.close()
|
||||
return cert_revoked
|
||||
|
||||
|
||||
class status:
|
||||
REVOKED = 1
|
||||
EXPIRED = 2
|
||||
VALID = 3
|
||||
|
||||
def __init__(self, status):
|
||||
self.value = status
|
||||
|
||||
def __str__(self):
|
||||
if self.value == self.EXPIRED:
|
||||
return "Expired"
|
||||
elif self.value == self.REVOKED:
|
||||
return "Revoked"
|
||||
elif self.value == self.VALID:
|
||||
return "Valid"
|
||||
else:
|
||||
return "Undefined"
|
254
step-ca-inspector.py
Executable file
254
step-ca-inspector.py
Executable file
|
@ -0,0 +1,254 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from tabulate import tabulate
|
||||
from models import ssh_cert, x509_cert
|
||||
|
||||
|
||||
def list_ssh_certs(sort_key, revoked=False, expired=False):
|
||||
cert_list = ssh_cert.list(sort_key=sort_key)
|
||||
cert_tbl = []
|
||||
for cert in cert_list:
|
||||
if cert.status.value == ssh_cert.status.EXPIRED and not expired:
|
||||
continue
|
||||
if cert.status.value == ssh_cert.status.REVOKED and not revoked:
|
||||
continue
|
||||
|
||||
cert_row = {}
|
||||
cert_row["Serial"] = cert.serial
|
||||
cert_row["Type"] = cert.type
|
||||
cert_row["Key ID"] = cert.key_id
|
||||
principals_count = len(cert.principals)
|
||||
principals_list = [x.decode() for x in cert.principals]
|
||||
if principals_count > 2:
|
||||
principals = principals_list[:2] + [f"+{principals_count - 2} more"]
|
||||
cert_row["Principals"] = "\n".join(principals)
|
||||
|
||||
validity = []
|
||||
validity.append(f"Not before: {cert.not_before}")
|
||||
validity.append(f"Not after: {cert.not_after}")
|
||||
if cert.revoked_at is not None:
|
||||
validity.append(f"Revoked at: {cert.revoked_at}")
|
||||
|
||||
cert_row["Validity"] = "\n".join(validity)
|
||||
cert_row["Status"] = cert.status
|
||||
|
||||
cert_tbl.append(cert_row)
|
||||
|
||||
print(tabulate(cert_tbl, headers="keys", tablefmt="fancy_grid"))
|
||||
|
||||
|
||||
def get_ssh_cert(serial):
|
||||
cert = ssh_cert.cert(serial)
|
||||
cert_tbl = []
|
||||
|
||||
cert_tbl.append(["Serial", cert.serial])
|
||||
cert_tbl.append(["Certificate type", cert.type])
|
||||
cert_tbl.append(["Certificate key type", cert.alg.decode()])
|
||||
public_key = f"{cert.public_key_type} SHA256:{cert.public_key_hash.decode()}"
|
||||
cert_tbl.append(["Public key", public_key])
|
||||
signing_key = f"{cert.signing_key_type} SHA256:{cert.signing_key_hash.decode()}"
|
||||
cert_tbl.append(["Signing key", signing_key])
|
||||
cert_tbl.append(["Key ID", cert.key_id.decode()])
|
||||
principals = [x.decode() for x in cert.principals]
|
||||
cert_tbl.append(["Principals", "\n".join(principals)])
|
||||
cert_tbl.append(["Not valid before", cert.not_before])
|
||||
cert_tbl.append(["Not valid after", cert.not_after])
|
||||
if cert.revoked_at is not None:
|
||||
cert_tbl.append(["Revoked at", cert.revoked_at])
|
||||
extensions = [x.decode() for x in cert.extensions]
|
||||
cert_tbl.append(["Extensions", "\n".join(extensions)])
|
||||
# cert_tbl.append(["Signing key", cert.signing_key.decode()])
|
||||
cert_tbl.append(["Status", cert.status])
|
||||
|
||||
print(tabulate(cert_tbl, tablefmt="fancy_grid"))
|
||||
|
||||
|
||||
def dump_ssh_cert(serial):
|
||||
cert = ssh_cert.cert(serial)
|
||||
print(cert.public_identity.decode())
|
||||
|
||||
|
||||
def list_x509_certs(sort_key, revoked=False, expired=False):
|
||||
cert_list = x509_cert.list(sort_key=sort_key)
|
||||
cert_tbl = []
|
||||
for cert in cert_list:
|
||||
if cert.status.value == x509_cert.status.EXPIRED and not expired:
|
||||
continue
|
||||
if cert.status.value == x509_cert.status.REVOKED and not revoked:
|
||||
continue
|
||||
|
||||
cert_row = {}
|
||||
cert_row["Serial"] = cert.serial
|
||||
cert_row["Subject"] = "%.30s" % cert.subject
|
||||
cert_row["Subject Alt Names (SAN)"] = "\n".join(
|
||||
["%.30s" % x for x in cert.san_names]
|
||||
)
|
||||
cert_row["Provisioner"] = (
|
||||
f"{cert.provisioner['name']} ({cert.provisioner['type']})"
|
||||
)
|
||||
validity = []
|
||||
validity.append(f"Not before: {cert.not_before}")
|
||||
validity.append(f"Not after: {cert.not_after}")
|
||||
if cert.revoked_at is not None:
|
||||
validity.append(f"Revoked at: {cert.revoked_at}")
|
||||
|
||||
cert_row["Validity"] = "\n".join(validity)
|
||||
cert_row["Status"] = cert.status
|
||||
|
||||
cert_tbl.append(cert_row)
|
||||
|
||||
print(tabulate(cert_tbl, headers="keys", tablefmt="fancy_grid"))
|
||||
|
||||
|
||||
def get_x509_cert(serial, show_pem=False):
|
||||
cert = x509_cert.cert(serial)
|
||||
cert_tbl = []
|
||||
|
||||
cert_tbl.append(["Serial", cert.serial])
|
||||
cert_tbl.append(["Subject", cert.subject])
|
||||
cert_tbl.append(["Subject Alt Names (SAN)", "\n".join(cert.san_names)])
|
||||
cert_tbl.append(["Issuer", cert.issuer])
|
||||
cert_tbl.append(["Not valid before", cert.not_before])
|
||||
cert_tbl.append(["Not valid after", cert.not_after])
|
||||
if cert.revoked_at is not None:
|
||||
cert_tbl.append(["Revoked at", cert.revoked_at])
|
||||
cert_tbl.append(
|
||||
["Provisioner", f"{cert.provisioner['name']} ({cert.provisioner['type']})"]
|
||||
)
|
||||
fingerprints = []
|
||||
fingerprints.append(f"MD5: {cert.md5.decode()}")
|
||||
fingerprints.append(f"SHA-1: {cert.sha1.decode()}")
|
||||
fingerprints.append(f"SHA-256: {cert.sha256.decode()}")
|
||||
cert_tbl.append(["Fingerprints", "\n".join(fingerprints)])
|
||||
cert_tbl.append(["Public key algorithm", cert.pub_alg])
|
||||
cert_tbl.append(["Signature algorithm", cert.sig_alg])
|
||||
cert_tbl.append(["Status", cert.status])
|
||||
# cert_tbl.append(["Extensions", cert.extensions])
|
||||
if show_pem:
|
||||
cert_tbl.append(["PEM", cert.pem.decode("utf-8")])
|
||||
|
||||
print(tabulate(cert_tbl, tablefmt="fancy_grid"))
|
||||
|
||||
|
||||
def dump_x509_cert(serial, cert_format="pem"):
|
||||
cert = x509_cert.cert(serial)
|
||||
print(cert.pem.decode("utf-8").rstrip())
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Step CA Inspector")
|
||||
subparsers = parser.add_subparsers(
|
||||
help="Object to inspect", dest="object", required=True
|
||||
)
|
||||
x509_parser = subparsers.add_parser("x509", help="x509 certificates")
|
||||
x509_subparsers = x509_parser.add_subparsers(
|
||||
help="Action for perform", dest="action", required=True
|
||||
)
|
||||
x509_list_parser = x509_subparsers.add_parser("list", help="List x509 certificates")
|
||||
x509_list_parser.add_argument(
|
||||
"--show-expired",
|
||||
"-e",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show expired certificates",
|
||||
)
|
||||
x509_list_parser.add_argument(
|
||||
"--show-revoked",
|
||||
"-r",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show revoked certificates",
|
||||
)
|
||||
x509_list_parser.add_argument(
|
||||
"--sort-by",
|
||||
"-s",
|
||||
type=str,
|
||||
choices=["not_after", "not_before"],
|
||||
default="not_after",
|
||||
help="Sort certificates",
|
||||
)
|
||||
x509_details_parser = x509_subparsers.add_parser(
|
||||
"details", help="Show an x509 certificate details"
|
||||
)
|
||||
x509_details_parser.add_argument(
|
||||
"--serial", "-s", type=str, required=True, help="Certificate serial"
|
||||
)
|
||||
x509_details_parser.add_argument(
|
||||
"--show-pem",
|
||||
"-p",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show PEM",
|
||||
)
|
||||
x509_dump_parser = x509_subparsers.add_parser("dump", help="Dump an x509 certificate")
|
||||
x509_dump_parser.add_argument(
|
||||
"--serial", "-s", type=str, required=True, help="Certificate serial"
|
||||
)
|
||||
x509_dump_parser.add_argument(
|
||||
"--format",
|
||||
"-f",
|
||||
type=str,
|
||||
choices=["pem"],
|
||||
required=False,
|
||||
help="Certificate format",
|
||||
)
|
||||
ssh_parser = subparsers.add_parser("ssh", help="ssh certificates")
|
||||
ssh_subparsers = ssh_parser.add_subparsers(
|
||||
help="Action for perform", dest="action", required=True
|
||||
)
|
||||
ssh_list_parser = ssh_subparsers.add_parser("list", help="List ssh certificates")
|
||||
ssh_list_parser.add_argument(
|
||||
"--show-expired",
|
||||
"-e",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show expired certificates",
|
||||
)
|
||||
ssh_list_parser.add_argument(
|
||||
"--show-revoked",
|
||||
"-r",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show revoked certificates",
|
||||
)
|
||||
ssh_list_parser.add_argument(
|
||||
"--sort-by",
|
||||
"-s",
|
||||
type=str,
|
||||
choices=["not_after", "not_before"],
|
||||
default="not_after",
|
||||
help="Sort certificates (default: not_after)",
|
||||
)
|
||||
ssh_details_parser = ssh_subparsers.add_parser(
|
||||
"details", help="Show an ssh certificate details"
|
||||
)
|
||||
ssh_details_parser.add_argument(
|
||||
"--serial", "-s", type=str, required=True, help="Certificate serial"
|
||||
)
|
||||
ssh_dump_parser = ssh_subparsers.add_parser("dump", help="Dump an ssh certificate")
|
||||
ssh_dump_parser.add_argument(
|
||||
"--serial", "-s", type=str, required=True, help="Certificate serial"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.object == "x509":
|
||||
if args.action == "list":
|
||||
list_x509_certs(
|
||||
revoked=args.show_revoked, expired=args.show_expired, sort_key=args.sort_by
|
||||
)
|
||||
elif args.action == "details":
|
||||
get_x509_cert(serial=args.serial, show_pem=args.show_pem)
|
||||
elif args.action == "dump":
|
||||
dump_x509_cert(serial=args.serial)
|
||||
elif args.object == "ssh":
|
||||
if args.action == "list":
|
||||
list_ssh_certs(
|
||||
revoked=args.show_revoked, expired=args.show_expired, sort_key=args.sort_by
|
||||
)
|
||||
elif args.action == "details":
|
||||
get_ssh_cert(serial=args.serial)
|
||||
elif args.action == "dump":
|
||||
dump_ssh_cert(serial=args.serial)
|
Loading…
Reference in a new issue