1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
//  CERTS.rs
//    by Lut99
//
//  Created:
//    30 Jan 2023, 09:35:00
//  Last edited:
//    26 Jul 2023, 09:35:32
//  Auto updated?
//    Yes
//
//  Description:
//!   Contains commands for managing certificates.
//

use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::{self, DirEntry, File, ReadDir};
use std::io::Write;
use std::path::{Path, PathBuf};

use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use brane_cfg::certs::load_all;
use brane_shr::formatters::PrettyListFormatter;
use console::{Alignment, pad_str, style};
use dialoguer::Confirm;
use enum_debug::EnumDebug;
use prettytable::Table;
use prettytable::format::FormatBuilder;
use rustls::{Certificate, PrivateKey};
use x509_parser::certificate::X509Certificate;
use x509_parser::extensions::{ParsedExtension, X509Extension};
use x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE;
use x509_parser::prelude::FromDer as _;
use x509_parser::x509::X509Name;

pub use crate::errors::CertsError as Error;
use crate::instance::InstanceInfo;
use crate::utils::{ensure_instances_dir, get_instance_dir};


/***** HELPER FUNCTIONS *****/
/// Resolves the given maybe-instance-name to a path and a name.
///
/// # Returns
/// The name and the path of the resolved instance.
///
/// # Errors
/// This function may error if the name given was unknown, or no active instance existed if no name was given.
fn resolve_instance(name: Option<String>) -> Result<(String, PathBuf), Error> {
    if let Some(name) = name {
        match get_instance_dir(&name) {
            Ok(path) => match path.exists() {
                true => Ok((name, path)),
                false => Err(Error::UnknownInstance { name }),
            },
            Err(err) => Err(Error::InstanceDirError { err }),
        }
    } else {
        match InstanceInfo::get_active_name() {
            Ok(name) => match InstanceInfo::get_instance_path(&name) {
                Ok(path) => Ok((name, path)),
                Err(err) => Err(Error::InstancePathError { name, err }),
            },
            Err(err) => Err(Error::ActiveInstanceReadError { err }),
        }
    }
}

/// Reads a certificate and extracts the issued usage and, if present, the domain for which it is intended.
///
/// # Arguments
/// - `cert`: The raw Certificate to analyze.
/// - `path`: The path to this certificate. Only used for debugging purposes.
/// - `i`: The number of this certificate in that file.
///
/// # Returns
/// A tuple of the issued usage and the name of the domain for which it is intended (or `None` if the latter was missing).
///
/// # Errors
/// This function may error if we failed to parse the certificate or extract the required fields.
fn analyse_cert(cert: &Certificate, path: impl Into<PathBuf>, i: usize) -> Result<(CertificateKind, Option<String>), Error> {
    // Attempt to parse the certificate as a real x509 one
    let cert: X509Certificate = match X509Certificate::from_der(&cert.0) {
        Ok((_, cert)) => cert,
        Err(err) => {
            return Err(Error::CertParseError { path: path.into(), i, err });
        },
    };

    // Try to find the list of allowed usages
    let exts: HashMap<_, _> = match cert.extensions_map() {
        Ok(exts) => exts,
        Err(err) => {
            return Err(Error::CertExtensionsError { path: path.into(), i, err });
        },
    };
    let usage: &X509Extension = match exts.get(&OID_X509_EXT_KEY_USAGE) {
        Some(usage) => usage,
        None => {
            return Err(Error::CertNoKeyUsageError { path: path.into(), i });
        },
    };

    // Attempt to find the CA one
    let kind: CertificateKind = match usage.parsed_extension() {
        ParsedExtension::KeyUsage(ext) => {
            let ds: bool = ext.digital_signature();
            let cs: bool = ext.crl_sign();
            if ds && !cs {
                CertificateKind::Client
            } else if !ds && cs {
                CertificateKind::Ca
            } else if ds && cs {
                CertificateKind::Both
            } else {
                return Err(Error::CertNoUsageError { path: path.into(), i });
            }
        },

        // Error values
        _ => {
            unreachable!();
        },
    };

    // Now attempt to extract the name from the issuer field
    let mut domain_name: Option<String> = None;
    let issuer: &X509Name = cert.issuer();
    for name in issuer.iter_common_name() {
        // Get it as a string
        let name: &str = match name.as_str() {
            Ok(name) => name,
            Err(err) => {
                return Err(Error::CertIssuerCaError { path: path.into(), i, err });
            },
        };

        // Extract the real name if any
        if name.len() >= 7 && &name[..7] == "CA for " {
            domain_name = Some(name[7..].into());
        }
    }

    // Done
    Ok((kind, domain_name))
}





/***** HELPER ENUMS *****/
/// Defines the possible certificate types we are interested in.
#[derive(Clone, Copy, Debug, EnumDebug, Eq, Hash, PartialEq)]
enum CertificateKind {
    /// It's both suited as a CA certificate _and_ a client certificate.
    Both,
    /// It's an authority certificate (used to verify the remote's identity)
    Ca,
    /// It's a client certificate (used to verify ourselves for the remote)
    Client,
}





/***** SERVICE FUNCTIONS *****/
/// Retrieves the path to the certificate directory of the active instance.
///
/// # Arguments
/// - `domain`: The name of the domain for which we want to get certificates.
///
/// # Returns
/// The path to the directory with the certificates of the active instance.
///
/// # Errors
/// This function may error if there was no active instance or we failed to get/read its directory.
pub fn get_active_certs_dir(domain: impl AsRef<Path>) -> Result<PathBuf, Error> {
    // Attempt to get the active link
    let active_path: PathBuf = match InstanceInfo::get_active_name() {
        Ok(name) => match InstanceInfo::get_instance_path(&name) {
            Ok(path) => path,
            Err(err) => {
                return Err(Error::InstancePathError { name, err });
            },
        },
        Err(err) => {
            return Err(Error::ActiveInstanceReadError { err });
        },
    };

    // Return the path within
    Ok(active_path.join("certs").join(domain))
}





/***** SUBCOMMANDS *****/
/// Adds the given certificate(s) as the certificate(s) for the given domain.
///
/// # Arguments
/// - `instance_name`: The name of the instance for which to add them. If omitted, we should default to the active instance.
/// - `paths`: The paths of the certificate files to add.
/// - `domain_name`: The name of the domain to add. If it is not present, then the function is supposed to deduce it from the given certificates.
/// - `force`: If given, does not ask for permission to override an existing certificate but just does it$^{TM}$.
///
/// # Errors
/// This function errors if we failed to read any of the certificates, parse them, if not all the required certificates were given, if we failed to write them and create the directory structure _or_ if we are asked to deduce the domain name but failed.
pub fn add(instance_name: Option<String>, paths: Vec<PathBuf>, mut domain_name: Option<String>, force: bool) -> Result<(), Error> {
    info!("Adding certificate file(s) '{:?}'...", paths);

    // Resolve the instance first
    let (instance_name, instance_path): (String, PathBuf) = resolve_instance(instance_name)?;
    debug!("Adding for instance: '{}' ({})", instance_name, instance_path.display());

    // First attempt to load the given certificates using rustls
    let mut ca_cert: Option<Certificate> = None;
    let mut client_cert: Option<Certificate> = None;
    let mut client_key: Option<PrivateKey> = None;
    for path in &paths {
        debug!("Reading certificate '{}'...", path.display());

        // Load any certificate and key we can find in this file
        let (certs, keys): (Vec<Certificate>, Vec<PrivateKey>) = match load_all(path) {
            Ok(res) => res,
            Err(err) => {
                return Err(Error::PemLoadError { path: path.clone(), err });
            },
        };
        if certs.is_empty() && keys.is_empty() {
            warn!("Empty file '{}' (at least, no valid certificates or keys found)", path.display());
            continue;
        }

        // We can add the keys by-default, since we know what they are used for
        for (i, key) in keys.into_iter().enumerate() {
            if client_key.is_some() {
                warn!("Multiple private keys specified, ignoring key {} in file '{}'", i, path.display());
                continue;
            }
            client_key = Some(key);
        }

        // Sort the certificates based on their allowed usage
        for (i, c) in certs.into_iter().enumerate() {
            // Attempt to extract the properties we are interested in from the certificate
            let (kind, cert_domain): (CertificateKind, Option<String>) = match analyse_cert(&c, path, i) {
                Ok(res) => res,
                Err(err) => {
                    warn!("{} (skipping)", err);
                    continue;
                },
            };
            debug!("Certificate {} in '{}' is a {} certificate for {:?}", i, path.display(), kind.variant(), cert_domain);

            // Do something with the domain name (i.e., store it or not
            if let Some(domain_name) = &domain_name {
                if let Some(cert_domain) = &cert_domain {
                    if cert_domain != domain_name {
                        warn!(
                            "Certificate {} in '{}' appears to be issued for domain '{}', but you are adding it for domain '{}'",
                            i,
                            path.display(),
                            cert_domain,
                            domain_name
                        );
                    }
                } else {
                    warn!("Certificate {} in '{}' does not have a domain name specified", i, path.display());
                }
            } else {
                domain_name = cert_domain;
            }

            // Then assign it to the relevant file(s)
            match kind {
                CertificateKind::Both => {
                    // Try to add as CA first
                    match ca_cert.is_some() {
                        true => {
                            warn!("Multiple CA certificates specified, ignoring certificate {} in file '{}'", i, path.display());
                            continue;
                        },
                        false => {
                            ca_cert = Some(c.clone());
                        },
                    }
                    // Next try as client
                    match client_cert.is_some() {
                        true => {
                            warn!("Multiple client certificates specified, ignoring certificate {} in file '{}'", i, path.display());
                            continue;
                        },
                        false => {
                            client_cert = Some(c);
                        },
                    }
                },
                CertificateKind::Ca => match ca_cert.is_some() {
                    true => {
                        warn!("Multiple CA certificates specified, ignoring certificate {} in file '{}'", i, path.display());
                        continue;
                    },
                    false => {
                        ca_cert = Some(c);
                    },
                },
                CertificateKind::Client => match client_cert.is_some() {
                    true => {
                        warn!("Multiple client certificates specified, ignoring certificate {} in file '{}'", i, path.display());
                        continue;
                    },
                    false => {
                        client_cert = Some(c);
                    },
                },
            }
        }
    }
    let ca_cert: Certificate = match ca_cert {
        Some(cert) => cert,
        None => {
            return Err(Error::NoCaCert);
        },
    };
    let client_cert: Certificate = match client_cert {
        Some(cert) => cert,
        None => {
            return Err(Error::NoClientCert);
        },
    };
    let client_key: PrivateKey = match client_key {
        Some(key) => key,
        None => {
            return Err(Error::NoClientKey);
        },
    };

    // Crash if the domain name is still unknown at this point
    let domain_name: String = match domain_name {
        Some(name) => name,
        None => {
            return Err(Error::NoDomainName);
        },
    };

    // Otherwise, start adding directory structures
    let certs_path: PathBuf = instance_path.join("certs").join(&domain_name);
    if certs_path.exists() {
        if !certs_path.is_dir() {
            return Err(Error::CertsDirNotADir { path: certs_path });
        }
        if !force {
            // Assert we are allowed to override it
            debug!("Asking for confirmation...");
            println!(
                "A certificate for domain {} in instance {} already exists. Overwrite?",
                style(&domain_name).cyan().bold(),
                style(&instance_name).cyan().bold()
            );
            let consent: bool = match Confirm::new().interact() {
                Ok(consent) => consent,
                Err(err) => {
                    return Err(Error::ConfirmationError { err });
                },
            };
            if !consent {
                println!("Not overwriting, aborted.");
                return Ok(());
            }
            if let Err(err) = fs::remove_dir_all(&certs_path) {
                return Err(Error::CertsDirRemoveError { path: certs_path, err });
            }
        }
    }

    debug!("Creating directory '{}'...", certs_path.display());
    if let Err(err) = fs::create_dir_all(&certs_path) {
        return Err(Error::CertsDirCreateError { path: certs_path, err });
    }

    // Now write the CA certificates first
    {
        let ca_path: PathBuf = certs_path.join("ca.pem");
        debug!("Writing CA certificates to '{}'...", ca_path.display());

        // Open a handle
        let mut handle: File = match File::create(&ca_path) {
            Ok(handle) => handle,
            Err(err) => {
                return Err(Error::FileOpenError { what: "ca", path: ca_path, err });
            },
        };

        // Write the CA certificate with all the bells and whistles
        if let Err(err) = writeln!(handle, "-----BEGIN CERTIFICATE-----") {
            return Err(Error::FileWriteError { what: "ca", path: ca_path, err });
        }
        for chunk in STANDARD.encode(ca_cert.0).as_bytes().chunks(64) {
            if let Err(err) = handle.write(chunk) {
                return Err(Error::FileWriteError { what: "ca", path: ca_path, err });
            }
            if let Err(err) = writeln!(handle) {
                return Err(Error::FileWriteError { what: "ca", path: ca_path, err });
            }
        }
        if let Err(err) = writeln!(handle, "-----END CERTIFICATE-----") {
            return Err(Error::FileWriteError { what: "ca", path: ca_path, err });
        }
    }

    // Next, write the client certificates and keys
    {
        let client_path: PathBuf = certs_path.join("client-id.pem");
        debug!("Writing client certificates & keys to '{}'...", client_path.display());

        // Open a handle
        let mut handle: File = match File::create(&client_path) {
            Ok(handle) => handle,
            Err(err) => {
                return Err(Error::FileOpenError { what: "client ID", path: client_path, err });
            },
        };

        // Write the client certificate with all the bells and whistles
        if let Err(err) = writeln!(handle, "-----BEGIN CERTIFICATE-----") {
            return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
        }
        for chunk in STANDARD.encode(client_cert.0).as_bytes().chunks(64) {
            if let Err(err) = handle.write(chunk) {
                return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
            }
            if let Err(err) = writeln!(handle) {
                return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
            }
        }
        if let Err(err) = writeln!(handle, "-----END CERTIFICATE-----") {
            return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
        }

        // Write the client key with all the bells and whistles
        if let Err(err) = writeln!(handle, "-----BEGIN RSA PRIVATE KEY-----") {
            return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
        }
        for chunk in STANDARD.encode(client_key.0).as_bytes().chunks(64) {
            if let Err(err) = handle.write(chunk) {
                return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
            }
            if let Err(err) = writeln!(handle) {
                return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
            }
        }
        if let Err(err) = writeln!(handle, "-----END RSA PRIVATE KEY-----") {
            return Err(Error::FileWriteError { what: "client ID", path: client_path, err });
        }
    }

    // Done!
    println!("Successfully added certificates for domain {} in instance {}", style(domain_name).cyan().bold(), style(instance_name).cyan().bold());
    Ok(())
}

/// Removes the certificate(s) for the given domain.
///
/// # Arguments
/// - `domain_names`: The name(s) of the domain(s) for which to remove the certificates.
/// - `instance_name`: The name of the instance for which to remove them. If omitted, we should default to the active instance.
/// - `force`: If given, does not ask for confirmation but just does it$^{TM}$.
///
/// # Errors
/// This function fails if we failed to find any directories or failed to remove them.
pub fn remove(domain_names: Vec<String>, instance_name: Option<String>, force: bool) -> Result<(), Error> {
    info!("Removing certificate file(s) '{:?}'...", domain_names);

    // Do nothing if no names are given
    if domain_names.is_empty() {
        println!("No domains given for which to remove certificates.");
        return Ok(());
    }

    // Resolve the instance first
    let (instance_name, instance_path): (String, PathBuf) = resolve_instance(instance_name)?;
    debug!("Removing for instance: '{}' ({})", instance_name, instance_path.display());

    // Ask the user for permission, if needed
    if !force {
        debug!("Asking for confirmation...");
        println!(
            "Are you sure you want to remove the certificates for domain{} {}?",
            if domain_names.len() > 1 { "s" } else { "" },
            PrettyListFormatter::new(domain_names.iter().map(|n| style(n).bold().cyan()), "and")
        );
        let consent: bool = match Confirm::new().interact() {
            Ok(consent) => consent,
            Err(err) => {
                return Err(Error::ConfirmationError { err });
            },
        };
        if !consent {
            println!("Aborted.");
            return Ok(());
        }
    }

    // We can continue, so let's remove them
    for name in domain_names {
        debug!("Removing certs for domain '{}' in instance '{}'...", name, instance_name);

        // Attempt to remove it if it exists
        let certs_dir: PathBuf = instance_path.join("certs").join(&name);
        if certs_dir.exists() {
            if let Err(err) = fs::remove_dir_all(&certs_dir) {
                warn!("Failed to remove directory '{}': {} (skipping)", certs_dir.display(), err);
                continue;
            }
        } else {
            println!("Domain {} does not have any certificates (skipping)", style(name).yellow().bold());
            continue;
        }

        // Alright done then
        println!("Removed certificates for domain {} in instance {}", style(name).cyan().bold(), style(&instance_name).cyan().bold());
    }

    // Done
    Ok(())
}



/// Lists the domains for which certificates are defined.
///
/// # Arguments
/// - `instance`: The name of the instance for which to list them. If omitted, we should default to the active instance.
/// - `all`: If given, shows all certificates across instances.
///
/// # Errors
/// This function fails if we failed to find any directories or failed to remove them.
pub fn list(instance_name: Option<String>, all: bool) -> Result<(), Error> {
    info!("Listing certificates...");

    // Prepare display table.
    let format = FormatBuilder::new().column_separator('\0').borders('\0').padding(1, 1).build();
    let mut table = Table::new();
    table.set_format(format);
    table.add_row(row!["INSTANCE", "DOMAIN", "CA", "CLIENT"]);

    // Find the instances to show
    let instances: Vec<(String, PathBuf)> = if all {
        // Get the instances dir
        debug!("Finding instances...");
        let instances_dir: PathBuf = match ensure_instances_dir(true) {
            Ok(dir) => dir,
            Err(err) => {
                return Err(Error::InstancesDirError { err });
            },
        };

        // Iterate over it
        let entries: ReadDir = match fs::read_dir(&instances_dir) {
            Ok(entries) => entries,
            Err(err) => {
                return Err(Error::DirReadError { what: "instances", path: instances_dir, err });
            },
        };
        let mut instances: Vec<(String, PathBuf)> = Vec::with_capacity(entries.size_hint().1.unwrap_or(entries.size_hint().0));
        for (i, entry) in entries.enumerate() {
            // Unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entries) => entries,
                Err(err) => {
                    return Err(Error::DirEntryReadError { what: "instances", path: instances_dir, entry: i, err });
                },
            };

            // Do some checks on whether this is an instance or not
            let entry_path: PathBuf = entry.path();
            if !entry_path.is_dir() {
                debug!("Skipping entry '{}' (not a directory)", entry_path.display());
                continue;
            }
            if !entry_path.join("info.yml").is_file() {
                debug!("Skipping entry '{}' (no nested info.yml file)", entry_path.display());
                continue;
            }

            // Now add the entry
            instances.push((entry.file_name().to_string_lossy().into(), entry_path));
        }

        // Return those
        instances
    } else {
        // Resolve the instance first
        let (instance_name, instance_path): (String, PathBuf) = resolve_instance(instance_name)?;
        vec![(instance_name, instance_path)]
    };

    // Search each of those instances for domains
    debug!("Finding domains in instances {:?}...", instances.iter().map(|(n, p)| format!("'{}' ({})", n, p.display())).collect::<Vec<String>>());
    for (name, path) in instances {
        // Ensure the certs directory exists
        let certs_dir: PathBuf = path.join("certs");
        if !certs_dir.exists() {
            if let Err(err) = fs::create_dir_all(&certs_dir) {
                return Err(Error::CertsDirCreateError { path: certs_dir, err });
            }
        }

        // Iterate over the things in the 'certs' directory
        let entries: ReadDir = match fs::read_dir(&certs_dir) {
            Ok(entries) => entries,
            Err(err) => {
                return Err(Error::DirReadError { what: "certificates", path: certs_dir, err });
            },
        };
        for (i, entry) in entries.enumerate() {
            // Unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entries) => entries,
                Err(err) => {
                    return Err(Error::DirEntryReadError { what: "certificates", path: certs_dir, entry: i, err });
                },
            };

            // Do some checks on whether this is a certificate directory or not
            let entry_path: PathBuf = entry.path();
            if !entry_path.is_dir() {
                debug!("Skipping entry '{}' (not a directory)", entry_path.display());
                continue;
            }
            let ca_path: PathBuf = entry_path.join("ca.pem");
            if !ca_path.is_file() {
                debug!("Skipping entry '{}' (no nested ca.pem file)", entry_path.display());
                continue;
            }
            let client_path: PathBuf = entry_path.join("client-id.pem");
            if !client_path.is_file() {
                debug!("Skipping entry '{}' (no nested client-id.pem file)", entry_path.display());
                continue;
            }

            // Cast the things to string
            let domain_name: String = entry.file_name().to_string_lossy().into();
            let ca_path: Cow<str> = ca_path.to_string_lossy();
            let client_path: Cow<str> = client_path.to_string_lossy();

            // Add an entry in the table
            let instance_name: Cow<str> = pad_str(&name, 20, Alignment::Left, Some(".."));
            let domain_name: Cow<str> = pad_str(&domain_name, 20, Alignment::Left, Some(".."));
            let ca_path: Cow<str> = pad_str(&ca_path, 30, Alignment::Left, Some(".."));
            let client_path: Cow<str> = pad_str(&client_path, 30, Alignment::Left, Some(".."));
            table.add_row(row![instance_name, domain_name, ca_path, client_path]);
        }
    }

    // Done
    table.printstd();
    Ok(())
}