use openpgp_card_rpgp::CardSlot;
use pgp::{
composed::SignedPublicKey,
packet::{
PublicKey,
PublicSubkey,
RevocationCode,
Signature,
SignatureType,
Subpacket,
SubpacketData,
UserId,
},
types::{Duration, KeyDetails, Tag, Timestamp},
};
use rpgpie::{
certificate::{Certificate, Checked},
merge::CertificateInfo,
};
use sop::{Password, errors::Error};
use crate::{
Certs,
Keys,
RPGSOPOCT,
card::{get_card, present_pin},
};
const SECONDS_PER_DAY: u32 = 24 * 60 * 60;
pub(crate) struct UpdateKey {
with_key_password: Vec<Password>,
merge: Vec<Certificate>,
}
impl UpdateKey {
pub(crate) fn new() -> Self {
Self {
with_key_password: Default::default(),
merge: Default::default(),
}
}
}
impl<'a> sop::ops::UpdateKey<'a, RPGSOPOCT, Certs, Keys> for UpdateKey {
fn signing_only(
self: Box<Self>,
) -> Box<dyn sop::ops::UpdateKey<'a, RPGSOPOCT, Certs, Keys> + 'a> {
unimplemented!("signing_only")
}
fn no_added_capabilities(
self: Box<Self>,
) -> Box<dyn sop::ops::UpdateKey<'a, RPGSOPOCT, Certs, Keys> + 'a> {
unimplemented!("no_added_capabilities")
}
fn with_key_password(
mut self: Box<Self>,
password: Password,
) -> sop::Result<Box<dyn sop::ops::UpdateKey<'a, RPGSOPOCT, Certs, Keys> + 'a>> {
self.with_key_password.push(password);
Ok(self)
}
fn merge_updates(
mut self: Box<Self>,
updates: &Certs,
) -> sop::Result<Box<dyn sop::ops::UpdateKey<'a, RPGSOPOCT, Certs, Keys> + 'a>> {
for cert in &updates.certs {
self.merge.push(cert.clone());
}
Ok(self)
}
fn update(self: Box<Self>, keys: &Keys) -> sop::Result<Keys> {
let now = Timestamp::now();
let mut res = Vec::new();
for key in &keys.keys {
let ccert: Checked = key.clone().into();
if ccert.revoked_at(now) {
return Err(sop::errors::Error::PrimaryKeyBad);
}
let mut spk: SignedPublicKey = key.clone().into();
let signer = &spk.primary_key;
log::info!(
"Trying to update this cert with signer: {:02x?}",
signer.fingerprint()
);
let mut card = get_card(&signer, openpgp_card::ocard::KeyType::Signing)
.map_err(|_| Error::UnspecifiedFailure)?;
let mut tx = card.transaction().expect("FIXME");
present_pin(
&mut tx,
openpgp_card::ocard::KeyType::Signing,
&self.with_key_password,
)
.expect("FIXME");
let cs =
CardSlot::init_from_card(&mut tx, openpgp_card::ocard::KeyType::Signing, &|| {
eprintln!("Touch confirmation required for rsop-oct update-key")
})
.expect("FIXME");
let selfsig = filter_direct(&spk.details.direct_signatures, &spk.primary_key);
if let Some(sig) = active_sig(&selfsig, now) {
if let Some(sig) = extend_sig(sig, &cs, spk.primary_key.created_at(), None, None) {
spk.details.direct_signatures.push(sig);
}
} else {
eprintln!("no active dks, not creating a new one")
}
for spsk in spk.public_subkeys.iter_mut() {
let selfsig = filter_subkey(&spsk.signatures, &spk.primary_key, &spsk.key);
if let Some(sig) = active_sig(&selfsig, now) {
if let Some(sig) =
extend_sig(sig, &cs, spsk.key.created_at(), Some(&spsk.key), None)
{
spsk.signatures.push(sig);
}
}
}
for su in spk.details.users.iter_mut() {
let selfsig = filter_userid(&su.signatures, &spk.primary_key, &su.id);
if let Some(sig) = active_sig(&selfsig, now) {
if let Some(sig) =
extend_sig(sig, &cs, spk.primary_key.created_at(), None, Some(&su.id))
{
su.signatures.push(sig);
}
}
}
let mut cert: Certificate = spk.into();
cert.merge(
self.merge
.iter()
.map(|c| CertificateInfo::Cert(c.clone()))
.collect(),
);
res.push(cert);
}
Ok(Keys {
keys: res,
source_name: None,
})
}
}
fn extend_sig(
sig: &Signature,
cs: &CardSlot,
created: Timestamp,
subkey: Option<&PublicSubkey>,
userid: Option<&UserId>,
) -> Option<Signature> {
let Some(config) = sig.config() else {
unimplemented!("FIXME")
};
if let Some(exp) = sig.key_expiration_time() {
if exp.as_secs() > 0 {
let now = Timestamp::now();
let target = Timestamp::from_secs(now.as_secs() + 30 * SECONDS_PER_DAY);
let valid_until = Timestamp::from_secs(created.as_secs() + exp.as_secs());
if valid_until < target {
let diff = target.as_secs() - valid_until.as_secs();
let years = diff.div_ceil(365 * SECONDS_PER_DAY);
let mut config_new = config.clone();
config_new.hashed_subpackets.retain(|s| {
!matches!(&s.data, SubpacketData::KeyExpirationTime(_))
&& !matches!(&s.data, SubpacketData::SignatureCreationTime(_))
});
config_new
.hashed_subpackets
.push(Subpacket::regular(SubpacketData::SignatureCreationTime(now)).unwrap());
let new_exp = Duration::from_secs(exp.as_secs() + years * 365 * SECONDS_PER_DAY);
config_new
.hashed_subpackets
.push(Subpacket::regular(SubpacketData::KeyExpirationTime(new_exp)).unwrap());
let sig = match (subkey, userid) {
(None, None) => {
eprintln!("Generating new direct key signature for primary");
config_new
.sign_key(cs, &pgp::types::Password::empty(), cs.public_key())
.expect("FIXME")
}
(Some(subkey), None) => {
eprintln!(
"Generating new subkey binding signature for {}",
subkey.fingerprint()
);
config_new
.sign_subkey_binding(
cs,
cs.public_key(),
&pgp::types::Password::empty(),
subkey,
)
.expect("FIXME")
}
(_, Some(userid)) => {
eprintln!(
"Generating new userid self-certification for '{}'",
String::from_utf8_lossy(userid.id())
);
config_new
.sign_certification(
cs,
cs.public_key(),
&pgp::types::Password::empty(),
Tag::UserId,
userid,
)
.expect("FIXME")
}
};
return Some(sig);
}
}
}
None
}
fn filter_direct<'a>(sigs: &'a [Signature], primary: &PublicKey) -> Vec<&'a Signature> {
sigs.iter()
.filter(|sig| sig.verify_key(primary).is_ok())
.collect()
}
fn filter_subkey<'a, 'b>(
sigs: &'a [Signature],
primary: &'b PublicKey,
subkey: &'b PublicSubkey,
) -> Vec<&'a Signature> {
sigs.iter()
.filter(|sig| sig.verify_subkey_binding(primary, subkey).is_ok())
.collect()
}
fn filter_userid<'a, 'b>(
sigs: &'a [Signature],
primary: &'b PublicKey,
userid: &'b UserId,
) -> Vec<&'a Signature> {
sigs.iter()
.filter(|sig| {
sig.verify_certification(primary, Tag::UserId, userid)
.is_ok()
})
.collect()
}
fn active_sig<'a>(self_sigs: &'a [&'a Signature], reference: Timestamp) -> Option<&'a Signature> {
fn is_soft_revocation_reason(reason: Option<&RevocationCode>) -> bool {
matches!(
reason,
Some(RevocationCode::KeyRetired)
| Some(RevocationCode::CertUserIdInvalid)
| Some(RevocationCode::KeySuperseded)
)
}
for sig in self_sigs {
if is_revocation(sig) {
if !is_soft_revocation_reason(sig.revocation_reason_code()) {
return None;
} else {
if sig.created().expect("signature creation time") <= reference {
return None;
}
}
}
}
self_sigs
.iter()
.filter(|s| !is_revocation(s))
.filter(|s| {
if let Some(created) = s.created() {
created <= reference
} else {
false
}
})
.max_by(|this, other| {
this.created()
.unwrap()
.as_secs()
.cmp(&other.created().unwrap().as_secs())
})
.copied()
}
fn is_revocation(sig: &Signature) -> bool {
let Some(typ) = sig.typ() else {
unimplemented!("no signature type")
};
matches!(
typ,
SignatureType::KeyRevocation
| SignatureType::CertRevocation
| SignatureType::SubkeyRevocation
)
}