use pgp::composed::{
ArmorOptions, CleartextSignedMessage, KeyType, SecretKeyParamsBuilder, SignedPublicKey,
SignedSecretKey, SubkeyParamsBuilder,
};
use pgp::crypto::hash::HashAlgorithm;
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::ser::Serialize as _;
use pgp::types::{Duration, KeyDetails, Password, Tag, Timestamp};
use rand::SeedableRng;
use rand::rngs::StdRng;
pub const DAY: u64 = 86_400;
pub fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after 1970")
.as_secs()
}
fn ts(secs: u64) -> Timestamp {
Timestamp::from_secs(u32::try_from(secs).expect("timestamp fits u32"))
}
pub struct Person {
pub secret: SignedSecretKey,
pub public: SignedPublicKey,
}
static KEYS_CREATED: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| now() - 400 * DAY);
impl Person {
pub fn new(seed: u64, name: &str) -> Self {
let mut rng = StdRng::seed_from_u64(seed);
let created = ts(*KEYS_CREATED);
let subkey = SubkeyParamsBuilder::default()
.key_type(KeyType::Ed25519Legacy)
.can_sign(true)
.created_at(created)
.build()
.expect("subkey params");
let mut params = SecretKeyParamsBuilder::default();
params
.key_type(KeyType::Ed25519Legacy)
.can_certify(true)
.can_sign(true)
.created_at(created)
.primary_user_id(format!("{name} <{}@kernel.example>", name.to_lowercase()))
.subkeys(vec![subkey]);
let secret = params
.build()
.expect("key params")
.generate(&mut rng)
.expect("generate key");
let public = secret.to_public_key();
Self { secret, public }
}
pub fn fingerprint(&self) -> String {
format!("{:X}", self.public.primary_key.fingerprint())
}
fn config(
&self,
typ: SignatureType,
created: u64,
expires_after: Option<u64>,
) -> SignatureConfig {
let key = &self.secret.primary_key;
let mut config = SignatureConfig::v4(typ, key.algorithm(), HashAlgorithm::Sha256);
config.hashed_subpackets = vec![
Subpacket::regular(SubpacketData::SignatureCreationTime(ts(created))).unwrap(),
Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint())).unwrap(),
];
if let Some(after) = expires_after {
config.hashed_subpackets.push(
Subpacket::regular(SubpacketData::SignatureExpirationTime(Duration::from_secs(
u32::try_from(after).unwrap(),
)))
.unwrap(),
);
}
config.unhashed_subpackets =
vec![Subpacket::regular(SubpacketData::IssuerKeyId(key.legacy_key_id())).unwrap()];
config
}
pub fn certification_of(
&self,
other: &Person,
created: u64,
expires_after: Option<u64>,
) -> pgp::packet::Signature {
self.config(SignatureType::CertGeneric, created, expires_after)
.sign_certification_third_party(
&self.secret.primary_key,
&Password::empty(),
&other.public.primary_key,
Tag::UserId,
&other.public.details.users[0].id,
)
.expect("certify")
}
pub fn certify(&self, other: &mut Person, created: u64, expires_after: Option<u64>) {
let sig = self.certification_of(other, created, expires_after);
other.public.details.users[0].signatures.push(sig);
}
pub fn revoke_certification(&self, other: &mut Person, created: u64) {
let sig = self
.config(SignatureType::CertRevocation, created, None)
.sign_certification_third_party(
&self.secret.primary_key,
&Password::empty(),
&other.public.primary_key,
Tag::UserId,
&other.public.details.users[0].id,
)
.expect("revoke certification");
other.public.details.users[0].signatures.push(sig);
}
pub fn revoke_key(&mut self, created: u64) {
let sig = self
.config(SignatureType::KeyRevocation, created, None)
.sign_key(
&self.secret.primary_key,
&Password::empty(),
&self.public.primary_key,
)
.expect("revoke key");
self.public.details.revocation_signatures.push(sig);
}
pub fn clearsign(&self, text: &str, created: u64, with_subkey: bool) -> String {
let msg = if with_subkey {
let sub = &self.secret.secret_subkeys[0].key;
let mut config =
SignatureConfig::v4(SignatureType::Text, sub.algorithm(), HashAlgorithm::Sha256);
config.hashed_subpackets = vec![
Subpacket::regular(SubpacketData::SignatureCreationTime(ts(created))).unwrap(),
Subpacket::regular(SubpacketData::IssuerFingerprint(sub.fingerprint())).unwrap(),
];
config.unhashed_subpackets =
vec![Subpacket::regular(SubpacketData::IssuerKeyId(sub.legacy_key_id())).unwrap()];
CleartextSignedMessage::new(text, config, sub, &Password::empty())
} else {
let config = self.config(SignatureType::Text, created, None);
CleartextSignedMessage::new(text, config, &self.secret.primary_key, &Password::empty())
}
.expect("clearsign");
msg.to_armored_string(ArmorOptions::default())
.expect("armor")
}
}
pub fn armored_keyring(people: &[&Person]) -> Vec<u8> {
people
.iter()
.map(|p| {
p.public
.to_armored_string(ArmorOptions::default())
.expect("armor key")
})
.collect::<Vec<_>>()
.join("\n")
.into_bytes()
}
pub fn binary_keyring(people: &[&Person]) -> Vec<u8> {
people
.iter()
.flat_map(|p| p.public.to_bytes().expect("serialise key"))
.collect()
}
pub fn link_text(did: &str) -> String {
format!(
"I link my OpenPGP key to my Linux Kernel community member identity.\n\
openvtc-link: {did}\n"
)
}