mirror of
https://github.com/chatmail/core.git
synced 2026-05-01 20:36:31 +03:00
feat: merge OpenPGP certificates and distribute relays in them
We put all relay addresses as a notation subpacket in the direct key signature to distribute the relay addresses.
This commit is contained in:
194
src/pgp.rs
194
src/pgp.rs
@@ -3,23 +3,25 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{BufRead, Cursor};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, KeyDetails, KeyVersion, Password, SigningKey as _, StringToKey,
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
};
|
||||
use rand_old::{Rng as _, thread_rng};
|
||||
use sha2::Sha256;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
@@ -420,6 +422,166 @@ pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
/// at most one User ID with exactly one signature.
|
||||
///
|
||||
/// See <https://openpgp.dev/book/adv/certificates.html#merging>
|
||||
/// and <https://openpgp.dev/book/adv/certificates.html#certificate-minimization>.
|
||||
///
|
||||
/// `new_certificate` does not necessarily contain newer data.
|
||||
/// It may come not directly from the key owner,
|
||||
/// e.g. via protected Autocrypt header or protected attachment
|
||||
/// in a signed message, but from Autocrypt-Gossip header or a vCard.
|
||||
/// Gossiped key may be older than the one we have
|
||||
/// or even have some packets maliciously dropped
|
||||
/// (for example, all encryption subkeys dropped)
|
||||
/// or restored from some older version of the certificate.
|
||||
pub fn merge_openpgp_certificates(
|
||||
old_certificate: SignedPublicKey,
|
||||
new_certificate: SignedPublicKey,
|
||||
) -> Result<SignedPublicKey> {
|
||||
old_certificate
|
||||
.verify_bindings()
|
||||
.context("First key cannot be verified")?;
|
||||
new_certificate
|
||||
.verify_bindings()
|
||||
.context("Second key cannot be verified")?;
|
||||
|
||||
// Decompose certificates.
|
||||
let SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
details: old_details,
|
||||
public_subkeys: old_public_subkeys,
|
||||
} = old_certificate;
|
||||
let SignedPublicKey {
|
||||
primary_key: new_primary_key,
|
||||
details: new_details,
|
||||
public_subkeys: _new_public_subkeys,
|
||||
} = new_certificate;
|
||||
|
||||
// Public keys may be serialized differently, e.g. using old and new packet type,
|
||||
// so we compare imprints instead of comparing the keys
|
||||
// directly with `old_primary_key == new_primary_key`.
|
||||
// Imprints, like fingerprints, are calculated over normalized packets.
|
||||
// On error we print fingerprints as this is what is used in the database
|
||||
// and what most tools show.
|
||||
let old_imprint = old_primary_key.imprint::<Sha256>()?;
|
||||
let new_imprint = new_primary_key.imprint::<Sha256>()?;
|
||||
ensure!(
|
||||
old_imprint == new_imprint,
|
||||
"Cannot merge certificates with different primary keys {} and {}",
|
||||
old_primary_key.fingerprint(),
|
||||
new_primary_key.fingerprint()
|
||||
);
|
||||
|
||||
// Decompose old and the new key details.
|
||||
//
|
||||
// Revocation signatures are currently ignored so we do not store them.
|
||||
//
|
||||
// User attributes are thrown away on purpose,
|
||||
// the only defined in RFC 9580 attribute is the Image Attribute
|
||||
// (<https://www.rfc-editor.org/rfc/rfc9580.html#section-5.12.1>
|
||||
// which we do not use and do not want to gossip.
|
||||
let SignedKeyDetails {
|
||||
revocation_signatures: _old_revocation_signatures,
|
||||
direct_signatures: old_direct_signatures,
|
||||
users: old_users,
|
||||
user_attributes: _old_user_attributes,
|
||||
} = old_details;
|
||||
let SignedKeyDetails {
|
||||
revocation_signatures: _new_revocation_signatures,
|
||||
direct_signatures: new_direct_signatures,
|
||||
users: new_users,
|
||||
user_attributes: _new_user_attributes,
|
||||
} = new_details;
|
||||
|
||||
// Select at most one direct key signature, the newest one.
|
||||
let best_direct_key_signature: Option<Signature> = old_direct_signatures
|
||||
.into_iter()
|
||||
.chain(new_direct_signatures)
|
||||
.filter(|x: &Signature| x.verify_key(&old_primary_key).is_ok())
|
||||
.max_by_key(|x: &Signature|
|
||||
// Converting to seconds because `Ord` is not derived for `Timestamp`:
|
||||
// <https://github.com/rpgp/rpgp/issues/737>
|
||||
x.created().map_or(0, |ts| ts.as_secs()));
|
||||
let direct_signatures: Vec<Signature> = best_direct_key_signature.into_iter().collect();
|
||||
|
||||
// Select at most one User ID.
|
||||
//
|
||||
// We prefer User IDs marked as primary,
|
||||
// but will select non-primary otherwise
|
||||
// because sometimes keys have no primary User ID,
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
let best_user_signature: Option<Signature> = signatures
|
||||
.into_iter()
|
||||
.filter(|signature: &Signature| {
|
||||
signature
|
||||
.verify_certification(&old_primary_key, pgp::types::Tag::UserId, &id)
|
||||
.is_ok()
|
||||
})
|
||||
.max_by_key(|signature: &Signature| {
|
||||
signature.created().map_or(0, |ts| ts.as_secs())
|
||||
});
|
||||
best_user_signature.map(|signature| (id, signature))
|
||||
})
|
||||
.max_by_key(|(_id, signature)| signature.created().map_or(0, |ts| ts.as_secs()))
|
||||
.map(|(id, signature)| SignedUser {
|
||||
id,
|
||||
signatures: vec![signature],
|
||||
});
|
||||
let users: Vec<SignedUser> = best_user.into_iter().collect();
|
||||
|
||||
let public_subkeys = old_public_subkeys;
|
||||
|
||||
Ok(SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
details: SignedKeyDetails {
|
||||
revocation_signatures: vec![],
|
||||
direct_signatures,
|
||||
users,
|
||||
user_attributes: vec![],
|
||||
},
|
||||
public_subkeys,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns relays addresses from the public key signature.
|
||||
///
|
||||
/// Not more than 3 relays are returned for each key.
|
||||
pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option<Vec<String>> {
|
||||
for signature in &public_key.details.direct_signatures {
|
||||
// The signature should be verified already when importing the key,
|
||||
// but we double-check here.
|
||||
let signature_is_valid = signature.verify_key(&public_key.primary_key).is_ok();
|
||||
debug_assert!(signature_is_valid);
|
||||
if signature_is_valid {
|
||||
for notation in signature.notations() {
|
||||
if notation.name == "relays@chatmail.at"
|
||||
&& let Ok(value) = str::from_utf8(¬ation.value)
|
||||
{
|
||||
return Some(
|
||||
value
|
||||
.split(",")
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| may_be_valid_addr(s))
|
||||
.take(3)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
@@ -818,4 +980,24 @@ mod tests {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_openpgp_certificates() {
|
||||
let alice = alice_keypair().to_public_key();
|
||||
let bob = bob_keypair().to_public_key();
|
||||
|
||||
// Merging certificate with itself does not change it.
|
||||
assert_eq!(
|
||||
merge_openpgp_certificates(alice.clone(), alice.clone()).unwrap(),
|
||||
alice
|
||||
);
|
||||
assert_eq!(
|
||||
merge_openpgp_certificates(bob.clone(), bob.clone()).unwrap(),
|
||||
bob
|
||||
);
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user