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