Implement hierarchical recipient policies (#5)

This commit is contained in:
Hermes Agent
2026-08-09 22:12:43 +00:00
parent 08bc244a4a
commit 34ce731716
7 changed files with 1284 additions and 0 deletions

View File

@@ -346,6 +346,47 @@ impl KeyStore {
.map_err(|_| CryptoError::EncryptionFailed)
}
/// Report whether every public-key session packet names exactly the requested primary
/// certificates. Unknown, anonymous, symmetric, missing, or extra recipients do not match.
pub fn is_encrypted_for(
&self,
ciphertext: &EncryptedEntry,
recipients: &[KeyHandle],
) -> Result<bool, CryptoError> {
let message = Message::from_bytes(Cursor::new(ciphertext.as_bytes()))
.map_err(|_| CryptoError::CorruptMessage)?;
let Message::Encrypted { esk, .. } = message else {
return Err(CryptoError::CorruptMessage);
};
let expected = recipients
.iter()
.map(|recipient| {
self.material(recipient)?;
Ok(recipient.0.clone())
})
.collect::<Result<BTreeSet<_>, CryptoError>>()?;
if expected.is_empty() {
return Ok(false);
}
let mut actual = BTreeSet::new();
for packet in esk {
let Esk::PublicKeyEncryptedSessionKey(packet) = packet else {
return Ok(false);
};
let matches = self
.keys
.iter()
.filter(|(_, material)| packet_matches_public(&packet, &material.public))
.map(|(fingerprint, _)| fingerprint)
.collect::<Vec<_>>();
if matches.len() != 1 {
return Ok(false);
}
actual.insert((*matches[0]).clone());
}
Ok(actual == expected)
}
/// Decrypt a pass entry with only the secret keys named by its PKESK packets.
pub fn decrypt(
&self,
@@ -760,6 +801,17 @@ fn message_matches_secret(message: &Message<'_>, key: &SignedSecretKey) -> bool
})
}
fn packet_matches_public(
packet: &pgp::packet::PublicKeyEncryptedSessionKey,
key: &SignedPublicKey,
) -> bool {
packet.match_identity(&key.primary_key)
|| key
.public_subkeys
.iter()
.any(|subkey| packet.match_identity(&subkey.key))
}
fn sign_data<K: SigningKey>(
key: &K,
password: &Password,