Skip to main content

openpgp_ca_lib/
lib.rs

1// SPDX-FileCopyrightText: 2019-2023 Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: GPL-3.0-or-later
3//
4// This file is part of OpenPGP CA
5// https://gitlab.com/openpgp-ca/openpgp-ca
6
7//! OpenPGP CA functionality as a library
8//!
9//! Example usage:
10//! ```
11//! # use openpgp_ca_lib::Uninit;
12//! # use tempfile;
13//! // all state of an OpenPGP CA instance is persisted in one SQLite database
14//! let db_filename = "/tmp/openpgp-ca.sqlite";
15//! # // for Doc-tests we need a random database filename
16//! # let file = tempfile::NamedTempFile::new().unwrap();
17//! # let db_filename = file.path().to_str().unwrap();
18//!
19//! // Set up a new, uninitialized OpenPGP CA database
20//! // (implicitly creates the database file).
21//! let ca_uninit = Uninit::new(Some(db_filename)).expect("Failed to set up CA");
22//!
23//! // Initialize the CA, create the CA key (with domain name and descriptive name)
24//! let ca = ca_uninit
25//!     .init_softkey("example.org", Some("Example Org OpenPGP CA Key"), None)
26//!     .unwrap();
27//!
28//! // Create a new user, certified by the CA, and a trust signature by the user
29//! // key on the CA key.
30//! //
31//! // The new private key for the user is printed to stdout and needs to be manually
32//! // processed from there.
33//! ca.user_new(
34//!     Some(&"Alice"),
35//!     &["alice@example.org"],
36//!     None,
37//!     false,
38//!     None,
39//!     false,
40//!     None,
41//!     true,
42//!     true,
43//!     false,
44//! )
45//! .unwrap();
46//! ```
47
48// FIXME: cleanup
49#![allow(non_local_definitions)]
50
51#[macro_use]
52extern crate diesel;
53
54#[macro_use]
55extern crate diesel_migrations;
56
57/// The version of this crate.
58pub const VERSION: &str = env!("CARGO_PKG_VERSION");
59
60mod backend;
61mod bridge;
62mod cert;
63pub mod db;
64mod export;
65pub mod pgp;
66mod revocation;
67mod secret;
68mod storage;
69pub mod types;
70mod update;
71
72use std::collections::HashMap;
73use std::env;
74use std::path::{Path, PathBuf};
75use std::rc::Rc;
76use std::str::FromStr;
77use std::time::SystemTime;
78
79use anyhow::{Context, Result};
80use chrono::offset::Utc;
81use chrono::DateTime;
82use openpgp_card::algorithm::AlgoSimple;
83use openpgp_card_pcsc::PcscBackend;
84use openpgp_card_sequoia::{state::Open, Card};
85use sequoia_openpgp::packet::{Signature, UserID};
86use sequoia_openpgp::parse::Parse;
87use sequoia_openpgp::Cert;
88
89use crate::backend::card::{check_card_empty, CardBackend};
90use crate::backend::softkey::SoftkeyBackend;
91use crate::backend::split::SplitCa;
92use crate::backend::{card, split, Backend};
93use crate::db::models;
94use crate::db::models::NewCacert;
95use crate::db::OcaDb;
96use crate::pgp::CipherSuite;
97use crate::secret::{CaSec, CaSecCB};
98use crate::storage::{CaStorageRW, DbCa, UninitDb};
99use crate::types::CertificationStatus;
100
101/// List of cards that are blank (no fingerprint in any slot)
102pub fn blank_cards() -> Result<Vec<String>> {
103    let mut idents = vec![];
104
105    for backend in PcscBackend::cards(None)? {
106        let mut card: Card<Open> = backend.into();
107        let transaction = card.transaction()?;
108
109        if check_card_empty(&transaction)? {
110            idents.push(transaction.application_identifier()?.ident());
111        }
112    }
113
114    Ok(idents)
115}
116
117/// List of cards that match the CA cert `cert`
118pub fn matching_cards(ca_cert: &[u8]) -> Result<Vec<String>> {
119    let ca_cert = Cert::from_bytes(ca_cert).context("Cert::from_bytes failed")?;
120
121    let mut idents = vec![];
122
123    for backend in PcscBackend::cards(None)? {
124        let mut card: Card<Open> = backend.into();
125        let mut transaction = card.transaction()?;
126
127        if card::card_matches(&mut transaction, &ca_cert).is_ok() {
128            idents.push(transaction.application_identifier()?.ident());
129        }
130    }
131
132    Ok(idents)
133}
134
135/// A CA instance that has a database, which is (possibly) not initialized yet.
136/// No backend for private key operations is available at this stage.
137pub struct Uninit {
138    storage: UninitDb,
139}
140
141/// An initialized OpenPGP CA instance, with a configured backend.
142/// Oca exposes the main functionality of OpenPGP CA.
143pub struct Oca {
144    storage: Box<dyn CaStorageRW>,
145    secret: Box<dyn CaSec>,
146
147    backend: Backend,
148    domainname: String,
149}
150
151impl Uninit {
152    /// Instantiate a new Uninit object (with db, but without private key backend).
153    ///
154    /// This CA may be fully uninitialized and not be linked to a CA key yet.
155    ///
156    /// The SQLite backend filename can be configured:
157    /// - explicitly via the db_url parameter, or
158    /// - the environment variable OPENPGP_CA_DB.
159    pub fn new(db_url: Option<&str>) -> Result<Self> {
160        let db_url = if let Some(url) = db_url {
161            url.to_owned()
162        } else if let Ok(database) = env::var("OPENPGP_CA_DB") {
163            database
164        } else {
165            return Err(anyhow::anyhow!("ERROR: no database configuration found"));
166        };
167
168        let db = Rc::new(OcaDb::new(&db_url)?);
169        db.diesel_migrations_run();
170
171        let storage = UninitDb::new(db);
172
173        Ok(Self { storage })
174    }
175
176    /// Check if domainname is legal according to Mozilla's Public Suffix List
177    fn check_domainname(domainname: &str) -> Result<()> {
178        // domainname syntax check
179        use addr::parser::DomainName;
180        use addr::psl::List;
181        if List.parse_domain_name(domainname).is_err() {
182            return Err(anyhow::anyhow!("Invalid domainname: '{}'", domainname));
183        }
184
185        Ok(())
186    }
187
188    /// Init CA with softkey backend.
189    ///
190    /// This generates a new OpenPGP Key for the Admin role and stores the
191    /// private Key in the OpenPGP CA database.
192    ///
193    /// `domainname` is the domain that this CA Admin is in charge of,
194    /// `name` is a descriptive name for the CA Admin
195    pub fn init_softkey(
196        self,
197        domainname: &str,
198        name: Option<&str>,
199        cipher_suite: Option<CipherSuite>,
200    ) -> Result<Oca> {
201        Self::check_domainname(domainname)?;
202        let (cert, _) = pgp::make_ca_cert(domainname, name, cipher_suite)?;
203
204        self.storage
205            .transaction(|| self.storage.ca_init_softkey(domainname, &cert))?;
206
207        self.init_from_db_state()
208    }
209
210    /// Init CA with OpenPGP card backend. Generate key material on the card.
211    ///
212    /// This assumes that:
213    /// - all key slots on the card are currently empty
214    /// - the PINs are set to their default values (User PIN is '123456', Admin PIN is '12345678')
215    ///
216    /// The User PIN is changed to a new, random 8-digit value and persisted in the CA database.
217    ///
218    /// The user is encouraged to change the Admin PIN to a different setting.
219    pub fn init_card_generate_on_card(
220        self,
221        ident: &str,
222        domain: &str,
223        name: Option<&str>,
224        algo: Option<AlgoSimple>,
225    ) -> Result<Oca> {
226        // The CA database must be uninitialized!
227        if self.storage.is_ca_initialized()? {
228            return Err(anyhow::anyhow!("CA database is already initialized"));
229        }
230
231        let email = format!("openpgp-ca@{domain}");
232        let uid = pgp::ca_user_id(&email, name);
233        let uid = String::from_utf8_lossy(uid.value()).to_string();
234
235        // Generate key material on card, get the public key,
236        // initialize the CA with these artifacts.
237        let (ca_cert, user_pin) = card::generate_on_card(ident, domain, uid, algo)?;
238
239        self.ca_init_card(ident, &user_pin, domain, &ca_cert)
240    }
241
242    pub fn init_card_generate_on_host(
243        self,
244        ident: &str,
245        domain: &str,
246        name: Option<&str>,
247        cipher_suite: Option<CipherSuite>,
248    ) -> Result<(Oca, String)> {
249        // The CA database must be uninitialized!
250        if self.storage.is_ca_initialized()? {
251            return Err(anyhow::anyhow!("CA database is already initialized"));
252        }
253
254        // Generate a new CA private key
255        let (ca_key, _) = pgp::make_ca_cert(domain, name, cipher_suite)?;
256
257        // Import key material to card.
258        let user_pin = card::import_to_card(ident, &ca_key)?;
259
260        // Private key material will get stripped implicitly by ca_init_card()
261        let ca = self.ca_init_card(ident, &user_pin, domain, &ca_key)?;
262
263        // return private key (unencrypted)
264        let key = pgp::cert_to_armored_private_key(&ca_key)?;
265
266        Ok((ca, key))
267    }
268
269    /// Import the CA's public key and use it with a pre-initialized OpenPGP card.
270    pub fn init_card_import_card(
271        self,
272        card_ident: &str,
273        user_pin: &str,
274        domain: &str,
275        ca_cert: &[u8],
276    ) -> Result<Oca> {
277        let ca_cert = Cert::from_bytes(ca_cert).context("Cert::from_bytes failed")?;
278
279        // Check if user-supplied PIN is accepted by the card
280        card::verify_user_pin(card_ident, user_pin)?;
281
282        // FIXME: could add checks if the cert and the keys on the card really correspond?
283        // (e.g.: could perform crypto operations on the card and test against cert?
284        // however, this might surprisingly require touch confirmation!)
285
286        self.ca_init_card(card_ident, user_pin, domain, &ca_cert)
287    }
288
289    /// Import existing CA private key onto a blank OpenPGP card.
290    pub fn init_card_import_key(
291        self,
292        card_ident: &str,
293        domain: &str,
294        ca_key: &[u8],
295    ) -> Result<Oca> {
296        let ca_key = Cert::from_bytes(ca_key).context("Cert::from_bytes failed")?;
297        if !ca_key.is_tsk() {
298            return Err(anyhow::anyhow!(
299                "No private key material found in file. Can't import to OpenPGP card."
300            ));
301        }
302
303        // FIXME: handle password protected key file?
304
305        // Import key material to card.
306        let user_pin = card::import_to_card(card_ident, &ca_key)?;
307
308        // Private key material will get stripped implicitly by ca_init_card()
309        self.ca_init_card(card_ident, &user_pin, domain, &ca_key)
310    }
311
312    /// Migrate an existing softkey CA onto a blank OpenPGP card.
313    ///
314    /// Caution: If you want to keep a backup of your CA private key material,
315    /// you need to make it before calling this!
316    ///
317    /// 1. The private CA key material gets imported to the blank OpenPGP card.
318    ///
319    /// 2. The CA is then switched from the softkey backend to the card backend. The CA private
320    ///    key material in the database is replaced with the CA public key material.
321    ///
322    /// 3. "VACUUM" is called on the database after removing the CA private key from the database.
323    ///    According to SQLite documentation, this will remove any traces of the key material from
324    ///    the database (however, no guarantees can be made about the underlying storage!).
325    pub fn migrate_card_import_key(self, card_ident: &str) -> Result<Oca> {
326        self.storage.transaction(|| {
327            let ca_key = self.storage.ca_get_cert_private()?;
328            if !ca_key.is_tsk() {
329                return Err(anyhow::anyhow!(
330                    "No private key material in CA database. Can't migrate to OpenPGP card."
331                ));
332            }
333
334            // Import key material to card.
335            let user_pin = card::import_to_card(card_ident, &ca_key)?;
336
337            // Switch cacert in db
338            let ca_pub = pgp::cert_to_armored(&ca_key.strip_secret_key_material())?;
339            CardBackend::ca_replace_in_place(&self.storage, card_ident, &user_pin, &ca_pub)?;
340
341            Ok(())
342        })?;
343
344        // Run VACUUM on sqlite.
345        // SQLite guarantees that this removes remaining private key fragments from the database file.
346        self.storage.vacuum()?;
347
348        self.init_from_db_state()
349    }
350
351    /// Init with OpenPGP card backend
352    fn ca_init_card(
353        self,
354        card_ident: &str,
355        pin: &str,
356        domainname: &str,
357        ca_cert: &Cert,
358    ) -> Result<Oca> {
359        Self::check_domainname(domainname)?;
360
361        self.storage.transaction(|| {
362            // The CA database must be uninitialized!
363            if self.storage.is_ca_initialized()? {
364                return Err(anyhow::anyhow!("CA database is already initialized"));
365            }
366
367            let pubkey = card::check_if_card_matches(card_ident, ca_cert)?;
368
369            CardBackend::ca_init(
370                &self.storage,
371                domainname,
372                card_ident,
373                pin,
374                &pubkey,
375                &ca_cert.fingerprint().to_hex(),
376            )
377        })?;
378
379        self.init_from_db_state()
380    }
381
382    /// Initialize OpenpgpCa object - this assumes a backend has previously been configured.
383    fn init_from_db_state(self) -> Result<Oca> {
384        // check database state of this CA
385        let (ca, cacert) = self.storage.ca_cert()?;
386
387        let backend = Backend::from_config(cacert.backend.as_deref())?;
388        let domainname = ca.domainname;
389
390        match &backend {
391            Backend::Softkey => {
392                let softkey = SoftkeyBackend::new(self.storage.ca_get_cert_private()?);
393
394                let ca_cert_pub = self.storage.ca_get_cert_pub()?;
395                let ca_sec = CaSecCB::new(Rc::new(softkey), ca_cert_pub);
396
397                let storage = Box::new(DbCa::new(self.storage.db()));
398
399                Ok(Oca {
400                    storage,
401                    secret: Box::new(ca_sec),
402                    backend,
403                    domainname,
404                })
405            }
406            Backend::Card(card) => {
407                let card_ca = CardBackend::new(&card.ident, &card.user_pin)?;
408
409                let ca_cert = self.storage.ca_get_cert_pub()?;
410                let ca_sec = CaSecCB::new(Rc::new(card_ca), ca_cert);
411
412                let storage = Box::new(DbCa::new(self.storage.db()));
413
414                Ok(Oca {
415                    storage,
416                    secret: Box::new(ca_sec),
417                    backend,
418                    domainname,
419                })
420            }
421            Backend::SplitFront => {
422                let oca_db = self.storage.db();
423
424                let storage = Box::new(DbCa::new(oca_db.clone()));
425                let secret = Box::new(SplitCa::new(oca_db)?);
426
427                Ok(Oca {
428                    storage,
429                    secret,
430                    backend,
431                    domainname,
432                })
433            }
434            Backend::SplitBack(inner) => {
435                let secret: Box<dyn CaSec> = match &**inner {
436                    Backend::Softkey => {
437                        let softkey = SoftkeyBackend::new(self.storage.ca_get_cert_private()?);
438                        let ca_cert_pub = self.storage.ca_get_cert_pub()?;
439                        Box::new(CaSecCB::new(Rc::new(softkey), ca_cert_pub))
440                    }
441                    Backend::Card(card) => {
442                        let card_ca = CardBackend::new(&card.ident, &card.user_pin)?;
443
444                        let ca_cert = self.storage.ca_get_cert_pub()?;
445                        Box::new(CaSecCB::new(Rc::new(card_ca), ca_cert))
446                    }
447
448                    _ => return Err(anyhow::anyhow!("Illegal inner backend: {}", inner)),
449                };
450
451                let db = match env::var("OPENPGP_CA_FRONT_DB") {
452                    Ok(readonly) => {
453                        println!("Using {readonly} as r/o online datasource");
454
455                        let ocadb = OcaDb::new(&readonly)?;
456                        split::SplitBackDb::new(Some(Rc::new(ocadb)))
457                    }
458                    Err(_e) => split::SplitBackDb::new(None),
459                };
460
461                let storage = Box::new(db);
462
463                Ok(Oca {
464                    storage,
465                    secret,
466                    backend,
467                    domainname,
468                })
469            }
470        }
471    }
472}
473
474impl Oca {
475    /// Open an initialized Oca instance.
476    ///
477    /// The SQLite backend filename can be configured:
478    /// - explicitly via the db_url parameter, or
479    /// - the environment variable OPENPGP_CA_DB.
480    pub fn open(db_url: Option<&str>) -> Result<Self> {
481        let cau = Uninit::new(db_url)?;
482        cau.init_from_db_state()
483    }
484
485    pub fn domainname(&self) -> &str {
486        &self.domainname
487    }
488
489    pub(crate) fn backend(&self) -> &Backend {
490        &self.backend
491    }
492
493    /// Get the CaSec implementation to run operations that need CA
494    /// private key material.
495    pub(crate) fn secret(&self) -> &dyn CaSec {
496        &*self.secret
497    }
498
499    /// Change which card backs an OpenPGP CA instance
500    /// (e.g. to switch to a replacement for a broken card).
501    pub fn set_card_backend(self, card_ident: &str, user_pin: &str) -> Result<()> {
502        let cacert = self.storage.cacert()?;
503
504        let b = Backend::from_config(cacert.backend.as_deref())?;
505        match b {
506            Backend::Card(_c) => {
507                // For now, we only allow switches from card-backend to card-backend
508
509                // Check if user-supplied PIN is accepted by the card
510                card::verify_user_pin(card_ident, user_pin)?;
511
512                // Check if the card exists and contains the correct CA key
513                let ca_cert = self.ca_get_cert_pub()?;
514                let _pubkey = card::check_if_card_matches(card_ident, &ca_cert)?;
515
516                // Update backend configuration in database
517                let ca_pub = pgp::cert_to_armored(&ca_cert)?;
518
519                let db = self.storage.into_uninit();
520                CardBackend::ca_replace_in_place(&db, card_ident, user_pin, &ca_pub)?;
521
522                Ok(())
523            }
524            Backend::Softkey => Err(anyhow::anyhow!(
525                "Setting card backend from softkey is not supported."
526            )),
527            Backend::SplitFront | Backend::SplitBack(_) => Err(anyhow::anyhow!(
528                "Setting card backend from split mode is not supported."
529            )),
530        }
531    }
532
533    // -------- CA
534
535    /// Generate revocations for the CA key, write to output file.
536    pub fn ca_generate_revocations(&self, output: PathBuf) -> Result<()> {
537        self.secret.ca_generate_revocations(output)
538    }
539
540    /// Ingest/merge in any new tsigs for our CA certificate from 'cert'
541    pub fn ca_import_tsig(&self, cert: &[u8]) -> Result<()> {
542        self.storage.ca_import_tsig(cert)
543    }
544
545    /// Get current CA certificate from storage.
546    /// This representation of the CA cert includes user certifications.
547    ///
548    /// Get from database storage, if possible - the cert will then contain all certifications
549    /// we know of. However, on split-mode backends, we don't rely on storage, unless we get
550    /// a readonly copy of the online CA. In this case, the CA certificate may lack some or all
551    /// certifications.
552    pub fn ca_get_cert_pub(&self) -> Result<Cert> {
553        match self.backend {
554            // In a split-mode backend instance, we can't rely on having an up-to-date copy
555            // of the CA certificate in storage.
556            Backend::SplitBack(_) => {
557                if let Ok(ca_cert) = self.storage.ca_get_cert_pub() {
558                    // If readonly front database is available, get CA cert from there
559                    Ok(ca_cert)
560                } else {
561                    // If not: get CA cert from secret backend
562                    self.secret.cert()
563                }
564            }
565            _ => self.storage.ca_get_cert_pub(),
566        }
567    }
568
569    /// Returns the public key of the CA as an armored String (see [Self::ca_get_cert_pub]).
570    pub fn ca_get_pubkey_armored(&self) -> Result<String> {
571        let cert = self.ca_get_cert_pub()?;
572
573        let ca_pub =
574            pgp::cert_to_armored(&cert).context("Failed to transform CA key to armored pubkey")?;
575
576        Ok(ca_pub)
577    }
578
579    /// Get the User ID of this CA
580    pub(crate) fn get_ca_userid(&self) -> Result<UserID> {
581        let cert = self.ca_get_cert_pub()?;
582        let uids: Vec<_> = cert.userids().collect();
583
584        if uids.len() != 1 {
585            return Err(anyhow::anyhow!("ERROR: CA has != 1 user_id"));
586        }
587
588        Ok(uids[0].userid().clone())
589    }
590
591    /// Get the email of this CA
592    pub fn get_ca_email(&self) -> Result<String> {
593        let uid = self.get_ca_userid()?;
594        let email = uid.email2()?;
595
596        if let Some(email) = email {
597            Ok(email.to_string())
598        } else {
599            Err(anyhow::anyhow!("CA user_id has no email"))
600        }
601    }
602
603    /// Print information about the Ca to stdout.
604    ///
605    /// This shows the domainname, fingerprint and creation time of this OpenPGP CA instance.
606    pub fn ca_show(&self) -> Result<()> {
607        let cert = self.secret().cert()?;
608
609        let created = cert.primary_key().key().creation_time();
610        let created: DateTime<Utc> = created.into();
611
612        println!("    CA Domain: {}", self.domainname());
613        println!("  Fingerprint: {}", cert.fingerprint());
614        println!("Creation time: {}", created.format("%F %T %Z"));
615
616        let backend = self.backend();
617        println!("   CA Backend: {backend}");
618
619        Ok(())
620    }
621
622    /// Print private key of the Ca to stdout.
623    ///
624    /// This operation is only supported for Softkey and SplitBack+Softkey instances.
625    pub fn ca_print_private(&self) -> Result<()> {
626        match &self.backend {
627            Backend::Softkey => {
628                // OK
629            }
630            Backend::SplitBack(inner) => match **inner {
631                Backend::Softkey => {
632                    // OK
633                }
634                _ => {
635                    // SplitBack instance that is not Softkey-based
636                    return Err(anyhow::anyhow!(
637                        "Operation unsupported for this backend type"
638                    ));
639                }
640            },
641            _ => {
642                return Err(anyhow::anyhow!(
643                    "Operation unsupported for this backend type"
644                ));
645            }
646        }
647
648        let ca_cert = self
649            .storage
650            .cacert()
651            .context("failed to load CA from database")?;
652        println!("{}", ca_cert.priv_cert);
653
654        Ok(())
655    }
656
657    /// Find all User IDs that have been certified by `ca_cert_old` and re-certify them
658    /// with the current CA key.
659    ///
660    /// This can be useful after CA key rotation: when the CA has a new key, `ca_re_certify` issues
661    /// fresh certifications for all previously CA-certified user certs.
662    pub fn ca_re_certify(&self, ca_cert_old: &[u8], validity_days: u64) -> Result<()> {
663        let ca_cert_old = pgp::to_cert(ca_cert_old)?;
664
665        cert::certs_re_certify(self, ca_cert_old, validity_days)
666    }
667
668    /// Split a CA instance into a pair of "front" and "back" CA instances.
669    ///
670    /// This operation is currently supported for softkey or card-backed CAs.
671    pub fn ca_split_into(self, front: &Path, back: &Path) -> Result<()> {
672        match self.backend {
673            Backend::Softkey | Backend::Card(_) => {
674                let uninit_orig = self.storage.into_uninit();
675                let (orig_ca, orig_cacert) = uninit_orig.ca_cert()?;
676
677                let cert = Cert::from_str(&orig_cacert.priv_cert)?;
678                let pub_ca_cert = pgp::cert_to_armored(&cert)?;
679
680                let fp = cert.fingerprint().to_hex();
681
682                let db = uninit_orig.db();
683                let db_url = db.url();
684
685                // The front instance gets all user/cert data (but no CA private key/card config).
686
687                // - Assert that all references from users to 'ca_id' point to "1"
688                for user in db.users_sorted_by_name()? {
689                    if user.ca_id != 1 {
690                        return Err(anyhow::anyhow!(
691                            "Splitting a multi-CA setup is not currently supported"
692                        ));
693                    }
694                }
695
696                // - Copy the database file to "front" CA file
697                std::fs::copy(db_url, front)?;
698
699                if let Some(url) = front.to_str() {
700                    let front = OcaDb::new(url)?;
701
702                    // - Remove cacerts and add a new one ('ca' entry stays unchanged)
703                    front.cacerts_delete()?;
704
705                    let backend = Backend::SplitFront.to_config();
706
707                    let new_ca_cert = NewCacert {
708                        active: true,
709                        ca_id: orig_ca.id,
710                        priv_cert: pub_ca_cert,
711                        fingerprint: &fp,
712                        backend: backend.as_deref(),
713                    };
714                    front.cacert_insert(&new_ca_cert)?;
715
716                    // - Vacuum (to remove traces of private key material, if any)
717                    front.vacuum()?;
718                } else {
719                    return Err(anyhow::anyhow!("Illegal front filename"));
720                }
721
722                // The back instance is a new, bare database that just gets the CA
723                // softkey (or card config)
724                let orig_back = Backend::from_config(orig_cacert.backend.as_deref())?;
725
726                let backend = Backend::SplitBack(Box::new(orig_back));
727
728                if let Some(url) = back.to_str() {
729                    let back = Uninit::new(Some(url))?;
730
731                    back.storage.ca_insert(
732                        &orig_ca.domainname,
733                        &orig_cacert.priv_cert,
734                        &fp,
735                        backend.to_config().as_deref(),
736                    )?;
737                } else {
738                    return Err(anyhow::anyhow!("Illegal back filename"));
739                }
740
741                Ok(())
742            }
743            _ => Err(anyhow::anyhow!(
744                "Splitting operation not supported for this backend type"
745            )),
746        }
747    }
748
749    /// Merge a back CA into a front CA instance, resulting in a regular ("non-split") CA.
750    pub fn ca_merge_split(self, back: &Path) -> Result<()> {
751        match self.backend {
752            Backend::SplitFront => {
753                // get inner backend and cacert data from the back instance
754                if let Some(url) = back.to_str() {
755                    let back = OcaDb::new(url)?;
756                    let (_back_ca, back_cacert) = back.get_ca()?;
757
758                    let orig_back = Backend::from_config(back_cacert.backend.as_deref())?;
759                    if let Backend::SplitBack(inner) = orig_back {
760                        // update backend and cacert in front database
761
762                        let mut front_cacert = self.storage.cacert()?;
763
764                        if front_cacert.fingerprint != back_cacert.fingerprint {
765                            return Err(anyhow::anyhow!(
766                                "Front {} and back {} instance use different CA fingerprints",
767                                front_cacert.fingerprint,
768                                back_cacert.fingerprint
769                            ));
770                        }
771
772                        // The back CA contains private key material (in softkey mode).
773                        // Start from the back CA Cert, merge in the public material from the front CA.
774                        // Use the resulting merged cert for the newly merged CA.
775                        let back_cert = pgp::to_cert(back_cacert.priv_cert.as_bytes())?;
776                        let front_cert = pgp::to_cert(front_cacert.priv_cert.as_bytes())?;
777
778                        let ca_merged = back_cert.merge_public(front_cert)?;
779
780                        front_cacert.priv_cert = pgp::cert_to_armored_private_key(&ca_merged)?;
781
782                        // The backend config of the merged CA is the "inner" backend type of the back instance
783                        front_cacert.backend = inner.to_config();
784
785                        let db = self.storage;
786                        db.cacert_update(&front_cacert)?;
787                    }
788
789                    Ok(())
790                } else {
791                    Err(anyhow::anyhow!(
792                        "Failed to use back instance path ({:?})",
793                        back
794                    ))
795                }
796            }
797
798            _ => Err(anyhow::anyhow!(
799                "Merge operation not supported for this backend type"
800            )),
801        }
802    }
803
804    /// Export certification requests for the backing CA in a simple human-readable output format
805    /// (inspired by <https://github.com/wiktor-k/airsigner/>, but with some adjustments!).
806    ///
807    /// The output file is a tar-archive:
808    /// - The archive contains a top-level file "csr.txt", which lists User IDs that should be
809    ///   certified.
810    /// - Current versions of all certs are provided in the tar in armored format, as individual
811    ///   files "certs/*fingerprint*".
812    ///
813    /// One design goal of this format is to make it easy to implement small (and thus more easily
814    /// auditable) certification services, which may use arbitrary underlying mechanisms
815    /// (and/or PGP implementations) for signing.
816    pub fn ca_split_export(&self, file: PathBuf) -> Result<()> {
817        match self.backend {
818            Backend::SplitFront => {
819                let cacert = self.storage.cacert()?;
820
821                let queue = self.storage.queue_not_done()?;
822                SplitCa::export_csr_queue(file, queue, &cacert.fingerprint)?;
823
824                Ok(())
825            }
826            _ => Err(anyhow::anyhow!(
827                "Operation is only supported on split mode front instances."
828            )),
829        }
830    }
831
832    /// Process certification requests in a SplitBack instance
833    ///
834    /// When "batch" is false, this fn is interactive.
835    ///
836    /// In interactive mode, it reads KeyEvents for user feedback
837    /// about certification operations.
838    pub fn ca_split_certify(&self, import: PathBuf, export: PathBuf, batch: bool) -> Result<()> {
839        match self.backend {
840            Backend::SplitBack(_) => split::certify(&*self.secret, import, export, batch),
841            _ => Err(anyhow::anyhow!(
842                "Operation is only supported on split mode back instances."
843            )),
844        }
845    }
846
847    /// Ingest the certifications that were generated by the split backend
848    pub fn ca_split_import(&self, file: PathBuf) -> Result<()> {
849        match self.backend {
850            Backend::SplitFront => split::ca_split_import(&*self.storage, file),
851            _ => Err(anyhow::anyhow!(
852                "Operation is only supported on split mode front instances."
853            )),
854        }
855    }
856
857    /// Show the currently not done entries in the queue of a split mode front instance
858    pub fn ca_split_show_queue(&self) -> Result<()> {
859        match self.backend {
860            Backend::SplitFront => split::ca_split_show_queue(&*self.storage),
861            _ => Err(anyhow::anyhow!(
862                "Operation is only supported on split mode front instances."
863            )),
864        }
865    }
866
867    // -------- users / certs
868
869    /// Get a list of all User Certs
870    pub fn user_certs_get_all(&self) -> Result<Vec<models::Cert>> {
871        let users = self.storage.users_sorted_by_name()?;
872        let mut user_certs = Vec::new();
873        for user in users {
874            user_certs.append(&mut self.get_certs_by_user(&user)?);
875        }
876        Ok(user_certs)
877    }
878
879    /// Which certs will be expired in 'days' days?
880    ///
881    /// If a cert is not "alive" now, it will not get returned as expiring
882    /// (otherwise old/abandoned certs would clutter the results)
883    pub fn certs_expired(&self, days: u64) -> Result<HashMap<models::Cert, Option<SystemTime>>> {
884        cert::certs_expired(self, days)
885    }
886
887    /// Check if this Cert has been certified by the CA Key, returns all
888    /// certified User IDs
889    pub fn cert_check_ca_sig(&self, cert: &models::Cert) -> Result<CertificationStatus> {
890        cert::cert_check_ca_sig(self, cert).context("Failed while checking CA sig")
891    }
892
893    /// Check if this Cert has tsigned the CA Key
894    pub fn cert_check_tsig_on_ca(&self, cert: &models::Cert) -> Result<bool> {
895        cert::cert_check_tsig_on_ca(self, cert).context("Failed while checking tsig on CA")
896    }
897
898    /// Check if this CA has tsigned the bridge cert
899    pub fn check_tsig_on_bridge(&self, bridge: &models::Bridge) -> Result<bool> {
900        let ca = self.ca_get_cert_pub()?;
901
902        if let Some(br) = self.storage.cert_by_id(bridge.cert_id)? {
903            let bridge_cert = pgp::to_cert(br.pub_cert.as_bytes())?;
904
905            Ok(cert::check_tsig_on_cert(&ca, &bridge_cert)?)
906        } else {
907            Err(anyhow::anyhow!(
908                "No public key found for bridge to '{}'",
909                bridge.email
910            ))
911        }
912    }
913
914    /// Check all Certs for certifications from the CA. If a certification
915    /// expires in less than `threshold_days` and it is not marked as
916    /// 'inactive', make a new certification that is good for
917    /// `validity_days` and update the Cert.
918    pub fn certs_refresh_ca_certifications(
919        &self,
920        threshold_days: u64,
921        validity_days: u64,
922    ) -> Result<()> {
923        cert::certs_refresh_ca_certifications(self, threshold_days, validity_days)
924    }
925
926    /// Create a new OpenPGP CA User.
927    /// ("Centralized key creation workflow")
928    ///
929    /// This generates a fresh OpenPGP key for the new User.
930    /// The private key is printed to stdout and NOT stored in OpenPGP CA.
931    /// The public key material (Cert) is stored in the OpenPGP CA database.
932    ///
933    /// The CA Cert is trust-signed by this new user key and the user
934    /// Cert is certified by the CA.
935    #[allow(clippy::too_many_arguments)]
936    pub fn user_new(
937        &self,
938        name: Option<&str>,
939        emails: &[&str],
940        duration_days: Option<u64>,
941        password: bool,
942        password_file: Option<String>,
943        output_format_minimal: bool,
944        cipher_suite: Option<CipherSuite>,
945        enable_encryption_subkey: bool,
946        enable_signing_subkey: bool,
947        enable_authentication_subkey: bool,
948    ) -> Result<()> {
949        // storage: ca_import_tsig + user_add
950        cert::user_new(
951            self,
952            name,
953            emails,
954            duration_days,
955            password,
956            password_file,
957            output_format_minimal,
958            cipher_suite,
959            enable_encryption_subkey,
960            enable_signing_subkey,
961            enable_authentication_subkey,
962        )
963    }
964
965    /// Import an existing OpenPGP Cert (public key) as a new OpenPGP CA user.
966    ///
967    /// The `cert` parameter accepts the user's armored public key.
968    ///
969    /// User IDs that correspond to `emails` will be signed by the CA.
970    ///
971    /// A symbolic `name` and a list of `emails` for this User can
972    /// optionally be supplied. If those are not set, emails are taken from
973    /// the list of User IDs in the public key. If the key has exactly one
974    /// User ID, the symbolic name is taken from that User ID.
975    ///
976    /// Optionally, revocation certificates can be supplied for storage in
977    /// OpenPGP CA.
978    pub fn cert_import_new(
979        &self,
980        cert: &[u8],
981        revoc_certs: &[&[u8]],
982        name: Option<&str>,
983        emails: &[&str],
984        duration_days: Option<u64>,
985    ) -> Result<()> {
986        cert::cert_import_new(self, cert, revoc_certs, name, emails, duration_days)
987    }
988
989    /// Update existing Cert in database (e.g. if the user has extended
990    /// the expiry date)
991    pub fn cert_import_update(&self, cert: &[u8]) -> Result<()> {
992        cert::cert_import_update(self, cert)
993    }
994
995    /// Mark a cert as "delisted" in the OpenPGP CA database.
996    /// As a result, the cert will not be exported to WKD anymore.
997    ///
998    /// Note: existing CA certifications will still get renewed for delisted
999    /// certs, but as the cert is not published via WKD, third parties might not
1000    /// learn about refreshed certifications.
1001    ///
1002    /// CAUTION:
1003    /// This method is probably rarely appropriate. In most cases, it's better
1004    /// to "deactivate" a cert (in almost all cases, it is best to continually
1005    /// serve the latest version of a cert to third parties, so they can learn
1006    /// about e.g. revocations on the cert)
1007    pub fn cert_delist(&self, fp: &str) -> Result<()> {
1008        self.storage.cert_delist(fp)
1009    }
1010
1011    /// Mark a certificate as "deactivated".
1012    /// It will continue to be listed and exported to WKD.
1013    /// However, the certification by our CA will expire and not get renewed.
1014    ///
1015    /// This approach is probably appropriate in most cases to phase out a
1016    /// certificate.
1017    pub fn cert_deactivate(&self, fp: &str) -> Result<()> {
1018        self.storage.cert_deactivate(fp)
1019    }
1020
1021    /// Get Cert by fingerprint.
1022    ///
1023    /// The fingerprint parameter is normalized (e.g. if it contains
1024    /// spaces, they will be filtered out).
1025    pub fn cert_get_by_fingerprint(&self, fingerprint: &str) -> Result<Option<models::Cert>> {
1026        let fp = pgp::normalize_fp(fingerprint)?;
1027
1028        self.storage.cert_by_fp(&fp)
1029    }
1030
1031    /// Get a list of all Certs for one User
1032    pub fn get_certs_by_user(&self, user: &models::User) -> Result<Vec<models::Cert>> {
1033        self.storage.certs_by_user(user)
1034    }
1035
1036    /// Get a list of all Users, ordered by name
1037    pub fn users_get_all(&self) -> Result<Vec<models::User>> {
1038        self.storage.users_sorted_by_name()
1039    }
1040
1041    /// Get a list of the Certs that are associated with `email`
1042    pub fn certs_by_email(&self, email: &str) -> Result<Vec<models::Cert>> {
1043        self.storage.certs_by_email(email)
1044    }
1045
1046    /// Get database User(s) for database Cert
1047    pub fn cert_get_users(&self, cert: &models::Cert) -> Result<Option<models::User>> {
1048        self.storage.user_by_cert(cert)
1049    }
1050
1051    /// Get the username that is associated with this Cert.
1052    ///
1053    /// The name is only for display purposes, it is set to "*no name*" if
1054    /// no name can be found, or to "*multiple users*" if the Cert is
1055    /// associated with more than one User.
1056    pub fn cert_get_name(&self, cert: &models::Cert) -> Result<String> {
1057        if let Some(user) = self.cert_get_users(cert)? {
1058            Ok(user.name.unwrap_or_else(|| "<no name>".to_string()))
1059        } else {
1060            Ok("<no name>".to_string())
1061        }
1062    }
1063
1064    pub fn print_certifications_status(&self) -> Result<()> {
1065        let mut count_ok = 0;
1066
1067        let db_users = self.users_get_all()?;
1068        for db_user in &db_users {
1069            for db_cert in self.get_certs_by_user(db_user)? {
1070                let sigs_by_ca = self.cert_check_ca_sig(&db_cert)?;
1071                let tsig_on_ca = self.cert_check_tsig_on_ca(&db_cert)?;
1072
1073                let sig_by_ca = !sigs_by_ca.certified.is_empty();
1074
1075                if sig_by_ca && tsig_on_ca {
1076                    count_ok += 1;
1077                } else {
1078                    println!(
1079                        "No mutual certification for {}{}:",
1080                        db_cert.fingerprint,
1081                        db_user
1082                            .name
1083                            .as_deref()
1084                            .map(|s| format!(" ({s})"))
1085                            .unwrap_or_else(|| "".to_string()),
1086                    );
1087
1088                    if !sig_by_ca {
1089                        println!("  No CA certification on any User ID");
1090                    }
1091
1092                    if !tsig_on_ca {
1093                        println!("  Has not tsigned CA key.");
1094                    };
1095
1096                    println!();
1097                }
1098            }
1099        }
1100
1101        println!(
1102            "Checked {} user keys, {} of them have mutual certifications.",
1103            db_users.len(),
1104            count_ok
1105        );
1106
1107        Ok(())
1108    }
1109
1110    pub fn print_expiry_status(&self, exp_days: u64) -> Result<()> {
1111        let expiries = self.certs_expired(exp_days)?;
1112
1113        if expiries.is_empty() {
1114            println!("No certificates will expire in the next {exp_days} days.");
1115        } else {
1116            println!(
1117                "The following {} certificate{} will expire in the next {} days.",
1118                expiries.len(),
1119                if expiries.len() == 1 { "" } else { "s" },
1120                exp_days
1121            );
1122            println!();
1123        }
1124
1125        for (db_cert, expiry) in expiries {
1126            let name = self.cert_get_name(&db_cert)?;
1127            println!("name {}, fingerprint {}", name, db_cert.fingerprint);
1128
1129            if let Some(exp) = expiry {
1130                let datetime: DateTime<Utc> = exp.into();
1131                println!(" expires: {}", datetime.format("%d/%m/%Y"));
1132            } else {
1133                println!(" no expiration date is set for this user key");
1134            }
1135
1136            println!();
1137        }
1138
1139        Ok(())
1140    }
1141
1142    pub fn print_users(&self) -> Result<()> {
1143        for db_user in self.users_get_all()? {
1144            for db_cert in self.get_certs_by_user(&db_user)? {
1145                let sig_by_ca = self.cert_check_ca_sig(&db_cert)?;
1146                let tsig_on_ca = self.cert_check_tsig_on_ca(&db_cert)?;
1147
1148                println!("OpenPGP certificate {}", db_cert.fingerprint);
1149                if let Some(name) = &db_user.name {
1150                    println!(" User '{name}'");
1151                }
1152
1153                if !sig_by_ca.certified.is_empty() {
1154                    println!(" Identities certified by this CA:");
1155                    for uid in sig_by_ca.certified {
1156                        println!(" - '{uid}'");
1157                    }
1158                }
1159
1160                if tsig_on_ca {
1161                    println!(" Has trust-signed this CA");
1162                }
1163
1164                let c = pgp::to_cert(db_cert.pub_cert.as_bytes())?;
1165
1166                match pgp::get_expiry(&c) {
1167                    Ok(Some(exp)) => {
1168                        let datetime: DateTime<Utc> = exp.into();
1169                        println!(" Expiration {}", datetime.format("%d/%m/%Y"));
1170                    }
1171                    Ok(None) => println!(" No expiration is set"),
1172                    Err(e) => println!(" Expiration unknown ({e})"),
1173                }
1174
1175                let revs = self.revocations_get(&db_cert)?;
1176                if !revs.is_empty() {
1177                    println!(" {} revocations available", revs.len());
1178                }
1179
1180                if pgp::is_possibly_revoked(&c) {
1181                    println!(" This certificate has (possibly) been REVOKED");
1182                }
1183                println!();
1184            }
1185        }
1186
1187        Ok(())
1188    }
1189
1190    // -------- revocations
1191
1192    /// Get a list of all Revocations for a cert
1193    pub fn revocations_get(&self, cert: &models::Cert) -> Result<Vec<models::Revocation>> {
1194        self.storage.revocations_by_cert(cert)
1195    }
1196
1197    /// Add a revocation certificate to the OpenPGP CA database.
1198    ///
1199    /// The matching cert is looked up by issuer Fingerprint, if
1200    /// possible - or by exhaustive search otherwise.
1201    ///
1202    /// Verifies that applying the revocation cert can be validated by the
1203    /// cert. Only if this is successful is the revocation stored.
1204    pub fn revocation_add(&self, revoc_cert: &[u8]) -> Result<()> {
1205        self.storage.revocation_add(revoc_cert)
1206    }
1207
1208    /// Add a revocation certificate to the OpenPGP CA database (from a file).
1209    pub fn revocation_add_from_file(&self, filename: &Path) -> Result<()> {
1210        let rev = std::fs::read(filename)?;
1211
1212        self.revocation_add(&rev)
1213    }
1214
1215    /// Get a Revocation by hash
1216    pub fn revocation_get_by_hash(&self, hash: &str) -> Result<models::Revocation> {
1217        if let Some(rev) = self.storage.revocation_by_hash(hash)? {
1218            Ok(rev)
1219        } else {
1220            Err(anyhow::anyhow!("No revocation found for {}", hash))
1221        }
1222    }
1223
1224    /// Apply a revocation.
1225    ///
1226    /// The revocation is merged into out copy of the OpenPGP Cert.
1227    pub fn revocation_apply(&self, revoc: models::Revocation) -> Result<()> {
1228        self.storage.revocation_apply(revoc)
1229    }
1230
1231    /// Get reason and creation time for a Revocation
1232    pub fn revocation_details(
1233        revocation: &models::Revocation,
1234    ) -> Result<(String, Option<SystemTime>)> {
1235        let rev = pgp::to_signature(revocation.revocation.as_bytes())?;
1236
1237        let creation = rev.signature_creation_time();
1238
1239        if let Some((code, reason)) = rev.reason_for_revocation() {
1240            let reason = String::from_utf8(reason.to_vec())?;
1241            Ok((format!("{code} ({reason})"), creation))
1242        } else {
1243            Ok(("Revocation reason unknown".to_string(), creation))
1244        }
1245    }
1246
1247    /// Get an armored representation of a revocation certificate
1248    pub fn revoc_to_armored(sig: &Signature) -> Result<String> {
1249        pgp::revoc_to_armored(sig, None)
1250    }
1251
1252    pub fn print_revocations(&self, email: &str) -> Result<()> {
1253        let certs = self.certs_by_email(email)?;
1254        if certs.is_empty() {
1255            println!("No OpenPGP keys found");
1256        } else {
1257            for cert in certs {
1258                let name = self.cert_get_name(&cert)?;
1259
1260                println!(
1261                    "Revocations for OpenPGP key {}, user \"{}\"",
1262                    cert.fingerprint, name
1263                );
1264                let revoc = self.revocations_get(&cert)?;
1265                for r in revoc {
1266                    let (reason, time) = Self::revocation_details(&r)?;
1267                    let time = if let Some(time) = time {
1268                        let datetime: DateTime<Utc> = time.into();
1269                        format!("{}", datetime.format("%d/%m/%Y"))
1270                    } else {
1271                        "".to_string()
1272                    };
1273                    println!(" - revocation id {}: {} ({})", r.hash, reason, time);
1274                    if r.published {
1275                        println!("   this revocation has been APPLIED");
1276                    }
1277
1278                    println!();
1279                }
1280            }
1281        }
1282        Ok(())
1283    }
1284
1285    // -------- emails
1286
1287    /// Get all Emails for a Cert
1288    pub fn emails_get(&self, cert: &models::Cert) -> Result<Vec<models::CertEmail>> {
1289        self.storage.emails_by_cert(cert)
1290    }
1291
1292    /// Get all Emails
1293    pub fn get_emails_all(&self) -> Result<Vec<models::CertEmail>> {
1294        self.storage.emails()
1295    }
1296
1297    // --------- bridges
1298
1299    /// Get a list of Bridges
1300    pub fn bridges_get(&self) -> Result<Vec<models::Bridge>> {
1301        self.storage.list_bridges()
1302    }
1303
1304    /// Get a specific Bridge
1305    pub fn bridges_search(&self, email: &str) -> Result<models::Bridge> {
1306        if let Some(bridge) = self.storage.bridge_by_email(email)? {
1307            Ok(bridge)
1308        } else {
1309            Err(anyhow::anyhow!("Bridge not found"))
1310        }
1311    }
1312
1313    /// Get the Cert row for a Bridge
1314    pub fn bridge_get_cert(&self, bridge: &models::Bridge) -> Result<models::Cert> {
1315        if let Some(cert) = self.storage.cert_by_id(bridge.cert_id)? {
1316            Ok(cert)
1317        } else {
1318            Err(anyhow::anyhow!("No cert found for bridge {}", bridge.id))
1319        }
1320    }
1321
1322    pub fn add_bridge(
1323        &self,
1324        email: Option<&str>,
1325        key_file: &Path,
1326        scope: Option<&str>,
1327        unscoped: bool,
1328    ) -> Result<(String, String)> {
1329        let (bridge, fingerprint) = bridge::bridge_new(self, key_file, email, scope, unscoped)?;
1330
1331        Ok((bridge.email, fingerprint.to_string()))
1332    }
1333
1334    /// Create a revocation Certificate for a Bridge and apply it the our
1335    /// copy of the remote CA's public key.
1336    ///
1337    /// Both the revoked remote public key and the revocation cert are
1338    /// printed to stdout.
1339    pub fn bridge_revoke(&self, email: &str) -> Result<()> {
1340        bridge::bridge_revoke(self, email)
1341    }
1342
1343    pub fn print_bridges(&self, email: Option<String>) -> Result<()> {
1344        let bridges = if let Some(email) = email {
1345            vec![self.bridges_search(&email)?]
1346        } else {
1347            self.bridges_get()?
1348        };
1349
1350        for bridge in bridges {
1351            println!("Bridge to '{}'", bridge.email);
1352            if let Some(db_cert) = self.storage.cert_by_id(bridge.cert_id)? {
1353                println!("{}", db_cert.pub_cert);
1354            }
1355            println!();
1356        }
1357
1358        Ok(())
1359    }
1360
1361    pub fn list_bridges(&self) -> Result<()> {
1362        for bridge in self.bridges_get()? {
1363            let tsigned = self.check_tsig_on_bridge(&bridge)?;
1364
1365            println!(
1366                "Bridge to '{}'{}, (scope: '{}')",
1367                bridge.email,
1368                if !tsigned {
1369                    " [no trust signature]"
1370                } else {
1371                    ""
1372                },
1373                bridge.scope,
1374            )
1375        }
1376
1377        Ok(())
1378    }
1379
1380    // -------- export
1381
1382    /// Export all user keys (that have a userid in `domain`) and the CA key
1383    /// into a wkd directory structure
1384    ///
1385    /// <https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-08>
1386    pub fn export_wkd(&self, domain: &str, path: &Path) -> Result<()> {
1387        export::wkd_export(self, domain, path)
1388    }
1389
1390    /// Export the contents of a CA in Keylist format.
1391    ///
1392    /// <https://code.firstlook.media/keylist-rfc-explainer>
1393    ///
1394    /// `path`: filesystem path into which the exported keylist and signature
1395    /// files will be written.
1396    ///
1397    /// `signature_uri`: the https address from which the signature file will
1398    /// be retrievable
1399    ///
1400    /// `force`: by default, this fn fails if the files exist; when force is
1401    /// true, overwrite.
1402    pub fn export_keylist(&self, path: PathBuf, signature_uri: String, force: bool) -> Result<()> {
1403        export::export_keylist(self, path, signature_uri, force)
1404    }
1405
1406    /// Export Certs from this CA into files, with filenames based on email
1407    /// addresses of user ids.
1408    pub fn export_certs_as_files(&self, email_filter: Option<String>, path: &str) -> Result<()> {
1409        export::export_certs_as_files(self, email_filter, path)
1410    }
1411
1412    pub fn print_certring(&self, email_filter: Option<String>) -> Result<()> {
1413        export::print_certring(self, email_filter)
1414    }
1415
1416    // -------- Update certs from public sources
1417
1418    /// Pull updates for all certs from WKD and merge them into our local
1419    /// storage.
1420    pub fn update_from_wkd(&self) -> Result<()> {
1421        for c in self.user_certs_get_all()? {
1422            match update::update_from_wkd(self, &c) {
1423                Ok(true) => {
1424                    println!("Got update for cert {}", c.fingerprint);
1425                }
1426                Ok(false) => {
1427                    println!("No changes for cert {}", c.fingerprint);
1428                }
1429                Err(e) => {
1430                    eprintln!("Failed to update cert {}: {}", c.fingerprint, e);
1431                }
1432            }
1433        }
1434        Ok(())
1435    }
1436
1437    /// Update all certs from the hagrid keyserver (<https://keys.openpgp.org/>)
1438    /// and merge any updates into our local storage for this cert.
1439    pub fn update_from_keyserver(&self) -> Result<()> {
1440        for c in self.user_certs_get_all()? {
1441            match update::update_from_hagrid(self, &c) {
1442                Ok(true) => {
1443                    println!("Got update for cert {}", c.fingerprint);
1444                }
1445                Ok(false) => {
1446                    println!("No changes for cert {}", c.fingerprint);
1447                }
1448                Err(e) => {
1449                    eprintln!("Failed to update cert {}: {}", c.fingerprint, e);
1450                }
1451            }
1452        }
1453        Ok(())
1454    }
1455}