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(", ")
)))
}