rpgpie-certificate-store 0.4.0

Certificate store for rpgpie, based on openpgp-cert-d
// SPDX-FileCopyrightText: Wiktor Kwapisiewicz <wiktor@metacode.biz>
// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Certificate lookup from online sources, like keyservers.

use crate::model::{Lookup, NewLookup};
use crate::schema::lookups;
use chrono::{NaiveDateTime, TimeDelta, Utc};
use diesel::prelude::*;
use rpgpie::certificate::Certificate;

// We consider earlier network lookups stale after this duration has elapsed
const STALE: TimeDelta = TimeDelta::days(1);

#[derive(Debug, Clone, Copy)]
enum Sources {
    Koo, // https://keys.openpgp.org/

    Ubuntu, // https://keyserver.ubuntu.com/

    #[allow(dead_code)]
    Sks, // hockeypuck-based synced pool (http://pgpkeys.eu)
}

impl From<Sources> for i32 {
    fn from(value: Sources) -> Self {
        match value {
            Sources::Koo => 1,
            Sources::Ubuntu => 2,
            Sources::Sks => 3,
        }
    }
}

const KOO_SEARCH: &str = "https://keys.openpgp.org/pks/lookup?op=get&options=mr&search=";

const UBUNTU_SEARCH: &str = "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x";

/// Get Certificates from keyservers for a set of identifiers
pub(crate) fn get_keyservers(
    identifiers: &[String],
    conn: &mut SqliteConnection,
) -> Vec<Certificate> {
    let mut certs = vec![];

    for identifier in identifiers {
        // FIXME: query all sources, merge results

        let mut ubuntu = get_keyserver_ubuntu_single(identifier, conn);
        if !ubuntu.is_empty() {
            certs.append(&mut ubuntu);
        } else {
            let mut koo = get_keyserver_koo_single(identifier, conn);
            certs.append(&mut koo);
        }
    }

    certs
}

pub(crate) fn get_keyserver_koo_single(
    identifier: &str,
    conn: &mut SqliteConnection,
) -> Vec<Certificate> {
    get_keyserver_single(identifier, KOO_SEARCH, Sources::Koo, conn)
}

pub(crate) fn get_keyserver_ubuntu_single(
    identifier: &str,
    conn: &mut SqliteConnection,
) -> Vec<Certificate> {
    get_keyserver_single(identifier, UBUNTU_SEARCH, Sources::Ubuntu, conn)
}

/// Get Certificates from keys.openpgp.org, by identifier
fn get_keyserver_single(
    identifier: &str,
    url: &str,
    source: Sources,
    conn: &mut SqliteConnection,
) -> Vec<Certificate> {
    let search = format!("{url}{identifier}");
    log::info!("get_keyserver_single {:?}: {search}", source);

    if let Some(date) = recent_lookup(source, identifier, conn) {
        log::info!("  skipping due to recent lookup at {}", date);
        return vec![];
    }

    if let Ok(response) = reqwest::blocking::get(search) {
        touch(source, identifier, conn);

        if let Ok(text) = response.text() {
            if let Ok(loaded_certs) = Certificate::load(&mut std::io::Cursor::new(text.as_bytes()))
            {
                return loaded_certs;
            }
        }
    }

    vec![]
}

fn touch(source: Sources, identifier: &str, conn: &mut SqliteConnection) {
    let insert = NewLookup {
        source: source.into(),
        identity: identifier,
        last_poll: Utc::now().naive_utc(),
    };

    let inserted_row_count = diesel::insert_into(lookups::table)
        .values(&insert)
        .on_conflict((lookups::source, lookups::identity))
        .do_update()
        .set(lookups::last_poll.eq(diesel::dsl::now))
        .execute(conn);

    if inserted_row_count != Ok(1) {
        log::warn!("touch failed");
    }
}

/// Returns Some(NaiveDateTime) if a "recent" lookup has been tried for "source/identifier"
fn recent_lookup(
    source: Sources,
    identifier: &str,
    conn: &mut SqliteConnection,
) -> Option<NaiveDateTime> {
    if let Ok(lookup) = lookups::table
        .filter(
            lookups::source
                .eq(i32::from(source))
                .and(lookups::identity.eq(identifier)),
        )
        .first::<Lookup>(conn)
    {
        let now = Utc::now();

        let delta = now.signed_duration_since(lookup.last_poll.and_utc());
        if delta >= STALE {
            // we consider this lookup stale -> we return "no recent lookup"
            None
        } else {
            Some(lookup.last_poll)
        }
    } else {
        None
    }
}