Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

provenance/src/pgp/rpgp.rs

//! The OpenPGP verifier itself, over rpgp (`pgp =0.21.0`, pure Rust, no default
//! features). Built only with holger-provenance's `pgp` feature; see [`super`].

use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::public_key::PublicKeyAlgorithm;
use pgp::packet::SignatureType;
use pgp::types::KeyDetails;

use super::VerifiedSignature;
use crate::error::{ProvenanceError, Result};

/// The most bytes one pinned key may have (armored).
const MAX_KEY: usize = 1 << 20;
/// The most bytes one `.asc` may have.
const MAX_SIG: usize = 64 << 10;

/// One pinned OpenPGP key: its name (the trust bundle's `pgp-keys/<name>`, or a
/// floor key's `id`) and the key, its bindings checked.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PgpKey {
    pub name: String,
    /// The primary key's fingerprint, lower-case hex.
    pub fingerprint: String,
    key: SignedPublicKey,
}

fn untrusted_key(name: &str, why: impl std::fmt::Display) -> ProvenanceError {
    ProvenanceError::TrustRoot(format!("pgp key {name:?}: {why}"))
}

impl PgpKey {
    /// Read an OpenPGP public key (an armored `PUBLIC KEY BLOCK`, or binary packets).
    /// Refusals are [`ProvenanceError::TrustRoot`].
    pub fn from_armored(name: &str, bytes: &[u8]) -> Result<Self> {
        if bytes.len() > MAX_KEY {
            return Err(untrusted_key(
                name,
                format!("{} bytes is more than a key", bytes.len()),
            ));
        }
        let armored = bytes.trim_ascii_start().starts_with(b"-----BEGIN");
        let key = if armored {
            SignedPublicKey::from_armor_single(bytes).map(|(k, _)| k)
        } else {
            SignedPublicKey::from_bytes(bytes)
        }
        .map_err(|e| untrusted_key(name, format!("not an OpenPGP public key: {e}")))?;
        key.verify_bindings()
            .map_err(|e| untrusted_key(name, format!("its bindings do not verify: {e}")))?;
        if !key.details.revocation_signatures.is_empty() {
            return Err(untrusted_key(
                name,
                "the key carries a revocation signature; a revoked key is not pinned",
            ));
        }
        let fingerprint = format!("{:x}", key.fingerprint());
        Ok(Self {
            name: name.to_string(),
            fingerprint,
            key,
        })
    }
}

fn alg_name(a: PublicKeyAlgorithm) -> &'static str {
    match a {
        PublicKeyAlgorithm::RSA | PublicKeyAlgorithm::RSASign => "rsa",
        PublicKeyAlgorithm::DSA => "dsa",
        PublicKeyAlgorithm::ECDSA => "ecdsa",
        PublicKeyAlgorithm::EdDSALegacy | PublicKeyAlgorithm::Ed25519 => "eddsa",
        PublicKeyAlgorithm::Ed448 => "ed448",
        _ => "other",
    }
}

fn hash_ok(h: HashAlgorithm) -> bool {
    matches!(
        h,
        HashAlgorithm::Sha224
            | HashAlgorithm::Sha256
            | HashAlgorithm::Sha384
            | HashAlgorithm::Sha512
            | HashAlgorithm::Sha3_256
            | HashAlgorithm::Sha3_512
    )
}

/// One key that may have made a signature: a pinned key's primary, or one of its
/// signing subkeys.
enum Component<'a> {
    Primary(&'a SignedPublicKey),
    Sub(&'a pgp::composed::SignedPublicSubKey),
}

impl Component<'_> {
    fn fingerprint(&self) -> pgp::types::Fingerprint {
        match self {
            Component::Primary(k) => k.fingerprint(),
            Component::Sub(k) => k.fingerprint(),
        }
    }
    fn key_id(&self) -> pgp::types::KeyId {
        match self {
            Component::Primary(k) => k.legacy_key_id(),
            Component::Sub(k) => k.legacy_key_id(),
        }
    }
    fn algorithm(&self) -> PublicKeyAlgorithm {
        match self {
            Component::Primary(k) => k.algorithm(),
            Component::Sub(k) => k.algorithm(),
        }
    }
    fn verify(&self, sig: &DetachedSignature, content: &[u8]) -> pgp::errors::Result<()> {
        match self {
            Component::Primary(k) => sig.verify(*k, content),
            Component::Sub(k) => sig.verify(*k, content),
        }
    }
}

fn components(k: &PgpKey) -> Vec<Component<'_>> {
    let mut out = vec![Component::Primary(&k.key)];
    // A subkey signs only when its (verified) binding says it may.
    out.extend(
        k.key
            .public_subkeys
            .iter()
            .filter(|s| s.signatures.iter().any(|b| b.key_flags().sign()))
            .map(Component::Sub),
    );
    out
}

fn bad(w: impl Into<String>) -> ProvenanceError {
    ProvenanceError::Malformed(w.into())
}

/// Read every signature packet of an `.asc` (armored) or `.sig` (binary).
fn signatures(asc: &[u8]) -> Result<Vec<DetachedSignature>> {
    if asc.len() > MAX_SIG {
        return Err(bad(format!(
            "the OpenPGP signature is {} bytes, more than {MAX_SIG}",
            asc.len()
        )));
    }
    let many: Vec<pgp::errors::Result<DetachedSignature>> =
        if asc.trim_ascii_start().starts_with(b"-----BEGIN") {
            DetachedSignature::from_armor_many(asc)
                .map_err(|e| bad(format!("not an armored OpenPGP signature: {e}")))?
                .0
                .collect()
        } else {
            DetachedSignature::from_bytes_many(asc)
                .map_err(|e| bad(format!("not an OpenPGP signature: {e}")))?
                .collect()
        };
    let sigs = many
        .into_iter()
        .collect::<pgp::errors::Result<Vec<_>>>()
        .map_err(|e| bad(format!("an OpenPGP signature packet: {e}")))?;
    if sigs.is_empty() {
        return Err(ProvenanceError::Unsigned(
            "the .asc carries no OpenPGP signature packet".into(),
        ));
    }
    Ok(sigs)
}

/// ★ Verify a detached OpenPGP signature over `content` with the pinned `keys`.
///
/// The first signature packet that verifies with a pinned key wins. A signature
/// whose issuer is not pinned is [`ProvenanceError::SignerUntrusted`]
/// ("unverified"); one whose issuer is pinned and does not verify over these
/// bytes is [`ProvenanceError::SignatureInvalid`].
pub fn verify_detached(content: &[u8], asc: &[u8], keys: &[PgpKey]) -> Result<VerifiedSignature> {
    let sigs = signatures(asc)?;
    let mut invalid: Option<String> = None;
    let mut unknown: Vec<String> = Vec::new();
    for s in &sigs {
        let sig = &s.signature;
        match sig.typ() {
            Some(SignatureType::Binary) | Some(SignatureType::Text) => {}
            t => {
                return Err(bad(format!(
                    "an OpenPGP signature of type {t:?} is not a signature over a file"
                )))
            }
        }
        let hash = sig
            .hash_alg()
            .ok_or_else(|| bad("the OpenPGP signature names no hash"))?;
        if !hash_ok(hash) {
            return Err(ProvenanceError::SignatureInvalid(format!(
                "the OpenPGP signature uses {hash}, which is too weak to trust"
            )));
        }
        let fprs = sig.issuer_fingerprint();
        let ids = sig.issuer_key_id();
        if fprs.is_empty() && ids.is_empty() {
            return Err(bad("the OpenPGP signature names no issuer"));
        }
        let mut matched = false;
        for k in keys {
            for c in components(k) {
                let fp = c.fingerprint();
                let hit = if fprs.is_empty() {
                    ids.iter().any(|i| **i == c.key_id())
                } else {
                    fprs.iter().any(|f| **f == fp)
                };
                if !hit {
                    continue;
                }
                matched = true;
                match c.verify(s, content) {
                    Ok(()) => {
                        return Ok(VerifiedSignature {
                            key: k.name.clone(),
                            fingerprint: k.fingerprint.clone(),
                            signer_fingerprint: format!("{fp:x}"),
                            alg: format!(
                                "{}-{}",
                                alg_name(c.algorithm()),
                                hash.to_string().to_ascii_lowercase()
                            ),
                            created: sig.created().map(|t| i64::from(t.as_secs())),
                        })
                    }
                    Err(e) => {
                        invalid = Some(format!("with the pinned key {:?} ({fp:X}): {e}", k.name))
                    }
                }
            }
        }
        if !matched {
            unknown.push(match fprs.first() {
                Some(f) => format!("{f:X}"),
                None => ids
                    .first()
                    .map(|i| format!("key id {}", hex::encode_upper(i)))
                    .unwrap_or_default(),
            });
        }
    }
    if let Some(why) = invalid {
        return Err(ProvenanceError::SignatureInvalid(format!(
            "the OpenPGP signature does not verify over these bytes {why}"
        )));
    }
    let pinned: Vec<&str> = keys.iter().map(|k| k.name.as_str()).collect();
    Err(ProvenanceError::SignerUntrusted(format!(
        "unverified: the OpenPGP signature's issuer {} is not a pinned key (pinned: {pinned:?})",
        unknown.join(", ")
    )))
}