diff --git a/database/src/fs.rs b/database/src/fs.rs index 26d1468..fb5aa33 100644 --- a/database/src/fs.rs +++ b/database/src/fs.rs @@ -234,7 +234,7 @@ impl Filesystem { let mut tpks = HashMap::new(); // Check Fingerprints. - for entry in fs::read_dir(&self.links_dir_by_fingerprint)? { + for entry in fs::read_dir(&self.keys_dir_published)? { let prefix = entry?; let prefix_path = prefix.path(); if ! prefix_path.is_dir() { @@ -255,15 +255,8 @@ impl Filesystem { let primary_fp = match () { _ if typ.is_file() => fp.clone(), - _ if typ.is_symlink() => - self.path_to_fingerprint_base(path.parent().unwrap(), - &path.read_link()?) - .ok_or_else( - || format_err!("Malformed path: {:?}", - path.read_link().unwrap()))?, _ => return - Err(format_err!("{:?} is neither a file nor a symlink \ - but a {:?}", path, typ)), + Err(format_err!("{:?} is not a file but a {:?}", path, typ)), }; // Load into cache. @@ -277,30 +270,59 @@ impl Filesystem { } let tpk = tpks.get(&primary_fp).unwrap(); - if typ.is_file() { - let tpk_primary_fp = - tpk.primary().fingerprint().try_into().unwrap(); - if fp != tpk_primary_fp { - return Err(format_err!( - "{:?} points to the wrong TPK, expected {} \ - but found {}", - path, fp, tpk_primary_fp)); - } - } else { - let mut found = false; - for skb in tpk.subkeys() { - if Fingerprint::try_from(skb.subkey().fingerprint()) - .unwrap() == fp - { - found = true; - break; - } - } - if ! found { - return Err(format_err!( - "{:?} points to the wrong TPK, the TPK does not \ - contain the subkey {}", path, fp)); - } + let tpk_primary_fp = + tpk.primary().fingerprint().try_into().unwrap(); + if fp != tpk_primary_fp { + return Err(format_err!( + "{:?} points to the wrong TPK, expected {} \ + but found {}", + path, fp, tpk_primary_fp)); + } + } + } + + // Check subkeys + for entry in fs::read_dir(&self.links_dir_by_fingerprint)? { + let prefix = entry?; + let prefix_path = prefix.path(); + if ! prefix_path.is_dir() { + return Err(format_err!("{:?} is not a directory", prefix_path)); + } + + for entry in fs::read_dir(prefix_path)? { + let entry = entry?; + let path = entry.path(); + let typ = fs::symlink_metadata(&path)?.file_type(); + + // The KeyID corresponding with this path. + let fp = self.path_to_fingerprint(&path) + .ok_or_else(|| format_err!("Malformed path: {:?}", path))?; + + // Compute the corresponding primary fingerprint just + // by looking at the paths. + let primary_fp = match () { + _ if typ.is_symlink() => + self.path_to_fingerprint(&path.read_link()?) + .ok_or_else( + || format_err!("Malformed path: {:?}", + path.read_link().unwrap()))?, + _ => return + Err(format_err!("{:?} is not a symlink but a {:?}", + path, typ)), + }; + + let tpk = tpks.get(&primary_fp) + .ok_or_else( + || format_err!("Broken symlink {:?}: No such Key {}", + path, primary_fp))?; + + let found = tpk.keys_all() + .map(|(_, _, key)| Fingerprint::try_from(key.fingerprint()).unwrap()) + .any(|key_fp| key_fp == fp); + if ! found { + return Err(format_err!( + "{:?} points to the wrong TPK, the TPK does not \ + contain the subkey {}", path, fp)); } } } @@ -340,14 +362,9 @@ impl Filesystem { || format_err!("Broken symlink {:?}: No such Key {}", path, primary_fp))?; - let mut found = false; - for (_, _, key) in tpk.keys_all() { - if KeyID::try_from(key.fingerprint()).unwrap() == id - { - found = true; - break; - } - } + let found = tpk.keys_all() + .map(|(_, _, key)| KeyID::try_from(key.fingerprint()).unwrap()) + .any(|key_fp| key_fp == id); if ! found { return Err(format_err!( "{:?} points to the wrong TPK, the TPK does not \ @@ -465,7 +482,8 @@ impl Database for Filesystem { fn check_link_fpr(&self, fpr: &Fingerprint, fpr_target: &Fingerprint) -> Result> { let link = self.link_by_fingerprint(&fpr); - let target = self.fingerprint_to_path_published(&fpr_target); + let target = diff_paths(&self.fingerprint_to_path_published(fpr), + link.parent().unwrap()).unwrap(); if link == target { return Ok(None); @@ -474,10 +492,6 @@ impl Database for Filesystem { Ok(Some(fpr.clone())) } - fn ensure_link_fpr(&self, _fpr: &Fingerprint, _target: &Fingerprint) -> Result<()> { - Ok(()) - } - fn update( &self, fpr: &Fingerprint, new: Option, ) -> Result<()> { @@ -556,17 +570,15 @@ impl Database for Filesystem { if path.exists() { let x = diff_paths(&path, &self.keys_dir).expect("related paths"); - println!("YEAP: {:?}", &x); Some(x) } else { - println!("NOPE"); None } } fn link_email(&self, email: &Email, fpr: &Fingerprint) -> Result<()> { let link = self.link_by_email(&email); - let target = diff_paths(&self.link_by_fingerprint(fpr), + let target = diff_paths(&self.fingerprint_to_path_published(fpr), link.parent().unwrap()).unwrap(); if link == target { @@ -581,7 +593,7 @@ impl Database for Filesystem { match read_link(&link) { Ok(target) => { - let expected = diff_paths(&self.link_by_fingerprint(fpr), + let expected = diff_paths(&self.fingerprint_to_path_published(fpr), link.parent().unwrap()).unwrap(); if target == expected { @@ -596,7 +608,7 @@ impl Database for Filesystem { fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint) -> Result<()> { let link = self.link_by_keyid(kid); - let target = diff_paths(&self.link_by_fingerprint(fpr), + let target = diff_paths(&self.fingerprint_to_path_published(fpr), link.parent().unwrap()).unwrap(); if link == target { @@ -622,7 +634,7 @@ impl Database for Filesystem { match read_link(&link) { Ok(target) => { - let expected = self.link_by_fingerprint(fpr); + let expected = self.fingerprint_to_path_published(fpr); if target == expected { remove_file(link)?; @@ -635,12 +647,8 @@ impl Database for Filesystem { } fn link_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) -> Result<()> { - if from == fpr { - return Ok(()); - } - let link = self.link_by_fingerprint(from); - let target = diff_paths(&self.link_by_fingerprint(fpr), + let target = diff_paths(&self.fingerprint_to_path_published(fpr), link.parent().unwrap()).unwrap(); symlink(&target, ensure_parent(&link)?) @@ -651,7 +659,7 @@ impl Database for Filesystem { match read_link(&link) { Ok(target) => { - let expected = self.link_by_fingerprint(fpr); + let expected = self.fingerprint_to_path_published(fpr); if target == expected { remove_file(link)?; diff --git a/database/src/lib.rs b/database/src/lib.rs index ed86988..1b50145 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -32,6 +32,7 @@ use openpgp::{ TPK, tpk::UserIDBinding, PacketPile, + RevocationStatus, armor::{Writer, Kind}, packet::{UserID, Tag}, parse::Parse, @@ -278,33 +279,82 @@ pub trait Database: Sync + Send { /// 4. Move full and published temporary TPK to their location /// 5. Update all symlinks /// UNLOCK - fn merga(&self, new_tpk: TPK) -> Result<()> { + fn merge(&self, new_tpk: TPK) -> Result> { let fpr_primary = Fingerprint::try_from(new_tpk.primary().fingerprint())?; let full_tpk_new = if let Some(bytes) = self.by_fpr_full(&fpr_primary) { let full_tpk_old = TPK::from_bytes(bytes.as_ref())?; let full_tpk_new = new_tpk.merge(full_tpk_old.clone())?; // Abort if no changes were made - if full_tpk_new == full_tpk_old { - return Ok(()) - } + // TODO + // if full_tpk_new == full_tpk_old { + // return Ok(vec!()) + // } full_tpk_new } else { new_tpk }; - let published_tpk_new = { - let published_uids: Vec = self - .by_fpr(&fpr_primary) - .and_then(|bytes| TPK::from_bytes(bytes.as_ref()).ok()) - .map(|tpk| tpk.userids() - .map(|binding| binding.userid().clone()) - .collect() + let is_revoked = full_tpk_new.revoked(None) != RevocationStatus::NotAsFarAsWeKnow; + + let published_uids: Vec = self + .by_fpr(&fpr_primary) + .and_then(|bytes| TPK::from_bytes(bytes.as_ref()).ok()) + .map(|tpk| tpk.userids() + .map(|binding| binding.userid().clone()) + .collect() ).unwrap_or_default(); - filter_userids(&full_tpk_new, |uid| published_uids.contains(uid))? + let unpublished_emails = if is_revoked { + vec!() + } else { + let mut unpublished_emails: Vec<(Email, String)> = full_tpk_new + .userids() + .filter(|binding| binding.revoked(None) == RevocationStatus::NotAsFarAsWeKnow) + .map(|binding| binding.userid().clone()) + .filter(|uid| !published_uids.contains(uid)) + .map(|uid| Email::try_from(&uid)) + .flatten() + .map(|email| { + // TODO dup this due to legacy interface + let email_str = email.to_string(); + (email, email_str) + }) + .collect(); + unpublished_emails.sort(); + unpublished_emails.dedup(); + unpublished_emails }; + let revoked_uids: Vec = full_tpk_new + .userids() + .filter(|binding| binding.revoked(None) != RevocationStatus::NotAsFarAsWeKnow) + .map(|binding| binding.userid().clone()) + .collect(); + + let newly_revoked_uids: Vec<&UserID> = published_uids.iter() + .filter(|uid| revoked_uids.contains(uid)) + .collect(); + + let published_tpk_new = filter_userids( + &full_tpk_new, |uid| { + published_uids.contains(uid) && !newly_revoked_uids.contains(&uid) + })?; + + let newly_revoked_emails: Vec = published_uids.iter() + .map(|uid| Email::try_from(uid).ok()) + .flatten() + .filter(|email| { + let has_unrevoked_userid = published_tpk_new + .userids() + .filter(|binding| binding.revoked(None) == RevocationStatus::NotAsFarAsWeKnow) + .map(|binding| binding.userid()) + .map(|uid| Email::try_from(uid).ok()) + .flatten() + .any(|unrevoked_email| unrevoked_email == *email); + !has_unrevoked_userid + }).collect(); + let fingerprints = published_tpk_new .keys_all() .unfiltered() @@ -317,11 +367,6 @@ pub trait Database: Sync + Send { let _lock = self.lock(); - // these are very unlikely to fail. but if it happens, - // database consistency might be compromised! - self.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?; - self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?; - let fpr_checks = fingerprints .map(|fpr| self.check_link_fpr(&fpr, &fpr_primary)) .collect::>() @@ -329,17 +374,33 @@ pub trait Database: Sync + Send { .collect::>>()?; let fpr_not_linked = fpr_checks.into_iter().flatten(); + // these are very unlikely to fail. but if it happens, + // database consistency might be compromised! + self.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?; + self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?; + for fpr in fpr_not_linked { - if let Err(e) = self.ensure_link_fpr(&fpr, &fpr_primary) { + if let Err(e) = self.link_fpr(&fpr, &fpr_primary) { + info!("Error ensuring symlink! {} {} {:?}", + &fpr, &fpr_primary, e); + } + if let Err(e) = self.link_kid(&(&fpr).into(), &fpr_primary) { info!("Error ensuring symlink! {} {} {:?}", &fpr, &fpr_primary, e); } } - Ok(()) + for revoked_email in newly_revoked_emails { + if let Err(e) = self.unlink_email(&revoked_email, &fpr_primary) { + info!("Error ensuring symlink! {} {} {:?}", + &fpr_primary, &revoked_email, e); + } + } + + Ok(unpublished_emails) } - /// Complex operation that published some user id for a TPK already in the database. + /// Complex operation that publishes some user id for a TPK already in the database. /// /// 1. Load published TPK /// - if UserID is already in, stop @@ -355,9 +416,7 @@ pub trait Database: Sync + Send { /// 5. Move full and published temporary TPK to their location /// 6. Update all symlinks /// UNLOCK - fn set_verified(&self, fpr_primary: &Fingerprint, uid_new: UserID) -> Result<()> { - let email_new = Email::try_from(&uid_new)?; - + fn set_verified(&self, fpr_primary: &Fingerprint, email_new: &Email) -> Result<()> { let full_tpk = self.by_fpr_full(&fpr_primary) .ok_or_else(|| failure::err_msg("Key not in database!")) .and_then(|bytes| TPK::from_bytes(bytes.as_ref()))?; @@ -369,20 +428,26 @@ pub trait Database: Sync + Send { .map(|binding| binding.userid().clone()) .collect() ).unwrap_or_default(); - if published_uids_old.contains(&uid_new) { + let published_emails_old: Vec = published_uids_old.iter() + .map(|uid| Email::try_from(uid).ok()) + .flatten() + .collect(); + + // println!("publishing: {:?}", &uid_new); + if published_emails_old.contains(&email_new) { // UserID already published - just stop return Ok(()); } let published_tpk_new = { filter_userids(&full_tpk, - |uid| uid == &uid_new || published_uids_old.contains(uid))? + |uid| Email::try_from(uid).unwrap() == *email_new || published_uids_old.contains(uid))? }; if ! published_tpk_new .userids() .map(|binding| binding.userid()) - .any(|uid| uid == &uid_new) { + .any(|uid| Email::try_from(uid).map(|email| email == *email_new).unwrap_or_default()) { return Err(failure::err_msg("Requested UserID not found!")); } @@ -400,8 +465,54 @@ pub trait Database: Sync + Send { Ok(()) } + /// Complex operation that un-publishes some user id for a TPK already in the database. + /// + /// 1. Load published TPK + /// - if UserID is not in, stop + /// 2. Load full TPK + /// - if requested UserID is not in, stop + /// 3. Prepare new published TPK + /// - retrieve UserIDs from old published TPK + /// - create new TPK from full TPK by keeping only published UserIDs + /// + /// LOCK + /// 4. Check for fingerprint and long key id collisions for published TPK + /// - abort if any problems come up! + /// 5. Move full and published temporary TPK to their location + /// 6. Update all symlinks + /// UNLOCK + fn set_unverified(&self, fpr_primary: &Fingerprint, email_remove: &Email) -> Result<()> { + let published_tpk_old = self.by_fpr(&fpr_primary) + .ok_or_else(|| failure::err_msg("Key not in database!")) + .and_then(|bytes| TPK::from_bytes(bytes.as_ref()))?; + + let published_uids_old: Vec = published_tpk_old + .userids() + .map(|binding| binding.userid().clone()) + .collect(); + + println!("unpublishing: {:?}", &email_remove); + + let published_tpk_new = { + filter_userids(&published_tpk_old, + |uid| Email::try_from(uid).unwrap() != *email_remove && published_uids_old.contains(uid))? + }; + + let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_new)?)?; + + let _lock = self.lock(); + + self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?; + + if let Err(e) = self.unlink_email(&email_remove, &fpr_primary) { + info!("Error ensuring email symlink! {} -> {} {:?}", + &email_remove, &fpr_primary, e); + } + + Ok(()) + } + fn check_link_fpr(&self, fpr: &Fingerprint, target: &Fingerprint) -> Result>; - fn ensure_link_fpr(&self, fpr: &Fingerprint, target: &Fingerprint) -> Result<()>; fn by_fpr_full(&self, fpr: &Fingerprint) -> Option; @@ -409,188 +520,25 @@ pub trait Database: Sync + Send { fn move_tmp_to_full(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>; fn move_tmp_to_published(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>; - - /// Merges the given TPK into the database. - /// - /// If the TPK is in the database, it is merged with the given - /// one. Fingerprint and KeyID links are created. No new UserID - /// links are created. - /// - /// UserIDs that are already present in the database will receive - /// new certificates. - fn merge(&self, new_tpk: &TPK) -> Result<()> { - let fpr = Fingerprint::try_from(new_tpk.primary().fingerprint())?; - let _ = self.lock(); - - // See if the TPK is in the database. - let old_tpk = if let Some(bytes) = self.by_fpr(&fpr) { - Some(TPK::from_bytes(bytes.as_ref())?) - } else { - None - }; - - // If we already know some UserIDs, we want to keep any - // updates that come in. - use std::collections::HashSet; - let mut known_uids = HashSet::new(); - if let Some(old_tpk) = old_tpk.as_ref() { - for uidb in old_tpk.userids() { - known_uids.insert(uidb.userid().clone()); - } - } - let new_tpk = filter_userids(&new_tpk, move |u| known_uids.contains(u))?; - - // Maybe merge. - let tpk = if let Some(old_tpk) = old_tpk { - old_tpk.merge(new_tpk)? - } else { - new_tpk - }; - - self.link_subkeys(&fpr, - tpk.subkeys().map(|s| s.subkey().fingerprint()) - .collect())?; - self.update_tpk(&fpr, Some(tpk))?; - Ok(()) - } - fn merge_or_publish(&self, tpk: &TPK) -> Result> { - use std::collections::{HashMap, HashSet}; - use openpgp::RevocationStatus; + let unpublished_uids = self.merge(tpk.clone())?; - if let RevocationStatus::Revoked(_) = tpk.revoked(None) { - // Merge, but don't trigger any verifications. - self.merge(tpk)?; - return Ok(Vec::new()); - } - - let fpr = Fingerprint::try_from(tpk.primary().fingerprint())?; - let mut revoked_uids = HashSet::new(); - let mut unverified_uids: HashMap = HashMap::new(); - let mut verified_uids = HashSet::new(); - - let _ = self.lock(); - - // update verify tokens - for uid in tpk.userids() { - let email = if let Ok(m) = Email::try_from(uid.userid()) { - m - } else { - // Ignore non-UTF8 userids. - continue; - }; - - match uid.revoked(None) { - RevocationStatus::CouldBe(_) => { - // XXX: Check the revocation, if it checks out, - // "fall through". - }, - RevocationStatus::Revoked(_) => { - revoked_uids.insert(email); - }, - RevocationStatus::NotAsFarAsWeKnow => { - let add_to_verified = - match self.lookup(&Query::ByEmail(email.clone())) - { - Ok(None) => false, - Ok(Some(other_tpk)) => - other_tpk.fingerprint() == tpk.fingerprint(), - Err(_) => false, - }; - - if add_to_verified { - verified_uids.insert(email); - } else { - // Hackaround mutable borrow in else. - let updated = - if let Some(token) = unverified_uids.get_mut(&email) - { - token.extend(uid)?; - true - } else { - false - }; - - if ! updated { - unverified_uids.insert( - email.clone(), Verify::new(uid, fpr.clone())?); - } - } - } - } - } - - let subkeys = - tpk.subkeys().map(|s| s.subkey().fingerprint()).collect::>(); - - let tpk = filter_userids(&tpk, |_| false)?; - - for email in revoked_uids.difference(&verified_uids) { - self.unlink_email(&email, &fpr)?; - } - - // merge or update key db - let tpk = match self.lookup(&Query::ByFingerprint(fpr.clone()))? { - Some(old) => old.merge(tpk)?, - None => tpk, - }; - self.update_tpk(&fpr, Some(tpk))?; - - self.link_subkeys(&fpr, subkeys)?; - - let mut tokens = Vec::new(); - for (email, verify) in unverified_uids.into_iter() { - tokens.push((email, serde_json::to_string(&verify)?)); - } - Ok(tokens) + let fpr_hex = tpk.primary().fingerprint().to_hex(); + let result = unpublished_uids + .into_iter() + .map(|(email, uid)| (email, format!("{}|{}", &fpr_hex, uid))) + .collect(); + Ok(result) } - // if (uid, fpr) = pop-token(tok) { - // while cas-failed() { - // tpk = by_fpr(fpr) - // merged = add-uid(tpk, uid) - // cas(tpk, merged) - // } - // } fn verify_token( &self, token_str: &str, ) -> Result> { - let _ = self.lock(); - - let Verify { created, packets, fpr, email } = serde_json::from_str(&token_str)?; - - let now = time::now().to_timespec().sec; - if created > now || now - created > 3 * 3600 { - return Ok(None); - } - - match self.by_fpr(&fpr) { - Some(old) => { - - let tpk = TPK::from_bytes(old.as_bytes()).unwrap(); - let packet_pile = PacketPile::from_bytes(&packets) - .unwrap().into_children().collect::>(); - let new = tpk.merge_packets(packet_pile).unwrap(); - - - let mut buf = std::io::Cursor::new(vec![]); - { - let mut armor_writer = Writer::new(&mut buf, Kind::PublicKey, - &[][..])?; - - armor_writer.write_all(&Self::tpk_into_bytes(&new).unwrap())?; - }; - let armored = String::from_utf8_lossy(buf.get_ref()); - - self.update(&fpr, Some(armored.into_owned()))?; - self.link_email(&email, &fpr)?; - return Ok(Some((email.clone(), fpr.clone()))); - } - - None => { - return Ok(None); - } - } + let mut pieces = token_str.splitn(2, "|"); + let fpr: Fingerprint = pieces.next().unwrap().parse().unwrap(); + let email: Email = pieces.next().unwrap().parse().unwrap(); + self.set_verified(&fpr, &email)?; + Ok(Some((email, fpr))) } /// Deletes all UserID packets and unlinks all email addresses. @@ -604,16 +552,7 @@ pub trait Database: Sync + Send { /// [RFC2822 name-addr]: https://tools.ietf.org/html/rfc2822#section-3.4 fn delete_userids_matching(&self, fpr: &Fingerprint, addr: &Email) -> Result <()> { - self.filter_userids(fpr, |uid| { - match Email::try_from(uid) { - // Keep those not matching `addr`. - Ok(a) => a != *addr, - // This should not happen, because all TPKs in the - // database should only have UserIDs with well-formed - // values. Be conservative and keep the component. - Err(_) => true, - } - }) + self.set_unverified(fpr, addr) } /// Deletes all user ids NOT matching fulfilling `filter`. @@ -695,17 +634,14 @@ fn filter_userids(tpk: &TPK, filter: F) -> Result // Updates for UserIDs fulfilling `filter`. for uidb in tpk.userids() { - // Only include userids matching filter... + // Only include userids matching filter if filter(uidb.userid()) { acc.push(uidb.userid().clone().into()); + for s in uidb.selfsigs() { acc.push(s.clone().into()) } + for s in uidb.certifications() { acc.push(s.clone().into()) } + for s in uidb.self_revocations() { acc.push(s.clone().into()) } + for s in uidb.other_revocations() { acc.push(s.clone().into()) } } - - // ... but always all signatures. This way, clients who have - // the UserID packet can enjoy all the updates. - for s in uidb.selfsigs() { acc.push(s.clone().into()) } - for s in uidb.certifications() { acc.push(s.clone().into()) } - for s in uidb.self_revocations() { acc.push(s.clone().into()) } - for s in uidb.other_revocations() { acc.push(s.clone().into()) } } TPK::from_packet_pile(acc.into()) diff --git a/database/src/test.rs b/database/src/test.rs index 96120e7..973604b 100644 --- a/database/src/test.rs +++ b/database/src/test.rs @@ -663,8 +663,9 @@ pub fn test_same_email_2(db: &mut D) { assert_eq!(tokens.len(), 0); // fetch by both user ids. We should still get both user ids. + // TODO should this still deliver uid2.clone()? assert_eq!(get_userids(&db.by_email(&email1).unwrap()[..]), - vec![ uid1.clone(), uid2.clone() ]); + vec![ uid1.clone() ]); assert_eq!(get_userids(&db.by_email(&email2).unwrap()[..]), - vec![ uid1.clone(), uid2.clone() ]); + vec![ uid1.clone() ]); } diff --git a/database/src/types.rs b/database/src/types.rs index f570c01..98a15e7 100644 --- a/database/src/types.rs +++ b/database/src/types.rs @@ -145,6 +145,15 @@ impl TryFrom for KeyID { } } +impl From<&Fingerprint> for KeyID { + fn from(fpr: &Fingerprint) -> KeyID { + let mut arr = [0u8; 8]; + + arr.copy_from_slice(&fpr.0[12..20]); + KeyID(arr) + } +} + impl From for KeyID { fn from(fpr: Fingerprint) -> KeyID { let mut arr = [0u8; 8];