allow lookup by keyid and subkey fpr

This commit is contained in:
Kai Michaelis 2019-01-04 14:07:14 +01:00
parent a5e9d8fa3c
commit 6d3ccd9762
9 changed files with 349 additions and 25 deletions

View File

@ -28,6 +28,11 @@ http {
try_files /$request_uri =404;
}
location ^~ /by-kid/ {
default_type application/pgp-keys;
try_files /$request_uri =404;
}
location = / {
proxy_pass http://127.0.0.1:8080;
}

View File

@ -10,7 +10,7 @@ use url;
use database::{Verify, Delete, Database};
use Result;
use types::{Email, Fingerprint};
use types::{Email, Fingerprint, KeyID};
pub struct Filesystem {
base: PathBuf,
@ -49,6 +49,7 @@ impl Filesystem {
create_dir_all(base.join("scratch_pad"))?;
create_dir_all(base.join("public").join("by-fpr"))?;
create_dir_all(base.join("public").join("by-email"))?;
create_dir_all(base.join("public").join("by-kid"))?;
info!("Opened base dir '{}'", base.display());
Ok(Filesystem{
@ -155,6 +156,64 @@ impl Database for Filesystem {
}
}
fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
let link = self.base.join("public").join("by-kid").join(kid.to_string());
if link.exists() {
let _ = remove_file(link.clone());
}
let _ = symlink(target, link);
}
fn unlink_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
let link = self.base.join("public").join("by-kid").join(kid.to_string());
match read_link(link.clone()) {
Ok(target) => {
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
if target == expected {
let _ = remove_file(link);
}
}
Err(_) => {}
}
}
fn link_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
let link = self.base.join("public").join("by-fpr").join(from.to_string());
if link == target { return; }
if link.exists() {
match link.metadata() {
Ok(ref meta) if meta.file_type().is_symlink() => {
let _ = remove_file(link.clone());
}
_ => {}
}
}
let _ = symlink(target, link);
}
fn unlink_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
let link = self.base.join("public").join("by-fpr").join(from.to_string());
match read_link(link.clone()) {
Ok(target) => {
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
if target == expected {
let _ = remove_file(link);
}
}
Err(_) => {}
}
}
fn pop_verify_token(&self, token: &str) -> Option<Verify> {
self.pop_token("verification_tokens", token).ok().and_then(|raw| {
str::from_utf8(&raw).ok().map(|s| s.to_string())
@ -211,6 +270,31 @@ impl Database for Filesystem {
}
})
}
// XXX: slow
fn by_kid(&self, kid: &KeyID) -> Option<Box<[u8]>> {
use std::fs;
let path = self.base.join("public").join("by-kid").join(kid.to_string());
fs::canonicalize(path).ok()
.and_then(|p| {
if p.starts_with(&self.base) {
Some(p)
} else {
None
}
}).and_then(|p| {
File::open(p).ok()
}).and_then(|mut fd| {
let mut buf = Vec::default();
if fd.read_to_end(&mut buf).is_ok() {
Some(buf.into_boxed_slice())
} else {
None
}
})
}
}
#[cfg(test)]
@ -257,4 +341,20 @@ mod tests {
test::test_uid_deletion(&mut db);
}
#[test]
fn subkey_lookup() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new(tmpdir.path()).unwrap();
test::test_subkey_lookup(&mut db);
}
#[test]
fn kid_lookup() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new(tmpdir.path()).unwrap();
test::test_kid_lookup(&mut db);
}
}

View File

@ -2,12 +2,15 @@ use std::collections::HashMap;
use parking_lot::Mutex;
use database::{Verify, Delete, Database};
use types::{Email, Fingerprint};
use types::{Email, Fingerprint, KeyID};
use Result;
#[derive(Debug)]
pub struct Memory {
fpr: Mutex<HashMap<Fingerprint, Box<[u8]>>>,
fpr_links: Mutex<HashMap<Fingerprint, Fingerprint>>,
email: Mutex<HashMap<Email, Fingerprint>>,
kid: Mutex<HashMap<KeyID, Fingerprint>>,
verify_token: Mutex<HashMap<String, Verify>>,
delete_token: Mutex<HashMap<String, Delete>>,
}
@ -16,6 +19,8 @@ impl Default for Memory {
fn default() -> Self {
Memory{
fpr: Mutex::new(HashMap::default()),
fpr_links: Mutex::new(HashMap::default()),
kid: Mutex::new(HashMap::default()),
email: Mutex::new(HashMap::default()),
verify_token: Mutex::new(HashMap::default()),
delete_token: Mutex::new(HashMap::default()),
@ -54,6 +59,14 @@ impl Database for Memory {
}
}
fn link_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
self.fpr_links.lock().insert(from.clone(), fpr.clone());
}
fn unlink_fpr(&self, from: &Fingerprint, _: &Fingerprint) {
self.fpr_links.lock().remove(from);
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) {
self.email.lock().insert(email.clone(), fpr.clone());
}
@ -62,6 +75,14 @@ impl Database for Memory {
self.email.lock().remove(email);
}
fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
self.kid.lock().insert(kid.clone(), fpr.clone());
}
fn unlink_kid(&self, kid: &KeyID, _: &Fingerprint) {
self.kid.lock().remove(kid);
}
// (verified uid, fpr)
fn pop_verify_token(&self, token: &str) -> Option<Verify> {
self.verify_token.lock().remove(token)
@ -73,15 +94,27 @@ impl Database for Memory {
}
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
self.fpr.lock().get(fpr).map(|x| x.clone())
let fprs = self.fpr.lock();
let links = self.fpr_links.lock();
fprs.get(fpr).map(|x| x.clone()).or_else(|| {
links.get(fpr).and_then(|fpr| fprs.get(fpr).map(|x| x.clone()))
})
}
fn by_email(&self, email: &Email) -> Option<Box<[u8]>> {
let by_email = self.email.lock();
let fprs = self.fpr.lock();
let by_email = self.email.lock();
by_email.get(email).and_then(|fpr| fprs.get(fpr).map(|x| x.clone()))
}
fn by_kid(&self, kid: &KeyID) -> Option<Box<[u8]>> {
let fprs = self.fpr.lock();
let by_kid = self.kid.lock();
by_kid.get(kid).and_then(|fpr| fprs.get(fpr).map(|x| x.clone()))
}
}
impl Memory {
@ -123,4 +156,18 @@ mod tests {
test::test_uid_verification(&mut db);
}
#[test]
fn subkey_lookup() {
let mut db = Memory::default();
test::test_subkey_lookup(&mut db);
}
#[test]
fn kid_lookup() {
let mut db = Memory::default();
test::test_kid_lookup(&mut db);
}
}

View File

@ -4,7 +4,7 @@ use std::convert::TryFrom;
use time;
use sequoia_openpgp::{packet::Signature, TPK, packet::UserID, Packet, PacketPile, constants::SignatureType, parse::Parse};
use Result;
use types::{Fingerprint, Email};
use types::{Fingerprint, Email, KeyID};
mod fs;
pub use self::fs::Filesystem;
@ -75,29 +75,35 @@ pub trait Database: Sync + Send {
fn link_email(&self, email: &Email, fpr: &Fingerprint);
fn unlink_email(&self, email: &Email, fpr: &Fingerprint);
fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint);
fn unlink_kid(&self, kid: &KeyID, fpr: &Fingerprint);
fn link_fpr(&self, from: &Fingerprint, to: &Fingerprint);
fn unlink_fpr(&self, from: &Fingerprint, to: &Fingerprint);
// (verified uid, fpr)
fn pop_verify_token(&self, token: &str) -> Option<Verify>;
// fpr
fn pop_delete_token(&self, token: &str) -> Option<Delete>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>>;
fn by_kid(&self, kid: &KeyID) -> Option<Box<[u8]>>;
fn by_email(&self, email: &Email) -> Option<Box<[u8]>>;
// fn by_kid<'a>(&self, fpr: &str) -> Option<&[u8]>;
fn strip_userids(tpk: TPK) -> Result<TPK> {
let pile = tpk.to_packet_pile().into_children().filter(|pkt| {
match pkt {
&Packet::PublicKey(_) | &Packet::PublicSubkey(_) => true,
&Packet::Signature(ref sig) =>
&Packet::Signature(ref sig) => {
sig.sigtype() == SignatureType::DirectKey
|| sig.sigtype() == SignatureType::SubkeyBinding
|| sig.sigtype() == SignatureType::PrimaryKeyBinding,
}
_ => false,
}
}).collect::<Vec<_>>();
TPK::from_packet_pile(PacketPile::from_packets(pile))
.map_err(|e| format!("sequoia_openpgp: {}", e).into())
.map_err(|e| format!("openpgp: {}", e).into())
}
fn tpk_into_bytes(tpk: &TPK) -> Result<Vec<u8>> {
@ -108,25 +114,48 @@ pub trait Database: Sync + Send {
tpk.serialize(&mut cur).map(|_| cur.into_inner()).map_err(|e| format!("{}", e).into())
}
fn link_subkeys(&self, fpr: &Fingerprint, subkeys: Vec<sequoia_openpgp::Fingerprint>) -> Result<()> {
// link (subkey) kid & and subkey fpr
self.link_kid(&fpr.clone().into(), &fpr);
for sub_fpr in subkeys {
let sub_fpr = Fingerprint::try_from(sub_fpr)?;
self.link_kid(&sub_fpr.clone().into(), &fpr);
self.link_fpr(&sub_fpr, &fpr);
}
Ok(())
}
fn merge_or_publish(&self, mut tpk: TPK) -> Result<Vec<(Email, String)>> {
use sequoia_openpgp::RevocationStatus;
let fpr = Fingerprint::try_from(tpk.primary().fingerprint())?;
let mut ret = Vec::default();
// update verify tokens
for uid in tpk.userids() {
let email = Email::try_from(uid.userid().clone())?;
match uid.revoked() {
RevocationStatus::Revoked(_) => { /* skip */ }
RevocationStatus::CouldBe(_) |
RevocationStatus::NotAsFarAsWeKnow => {
let email = Email::try_from(uid.userid().clone())?;
if self.by_email(&email).is_none() {
let payload = Verify::new(
uid.userid(),
&uid.selfsigs().collect::<Vec<_>>(),
fpr.clone())?;
if self.by_email(&email).is_none() {
let payload = Verify::new(
uid.userid(),
&uid.selfsigs().collect::<Vec<_>>(),
fpr.clone())?;
// XXX: send mail
ret.push((email, self.new_verify_token(payload)?));
ret.push((email, self.new_verify_token(payload)?));
}
}
}
}
let subkeys = tpk.subkeys().map(|s| s.subkey().fingerprint()).collect::<Vec<_>>();
tpk = Self::strip_userids(tpk)?;
for _ in 0..100 /* while cas failed */ {
@ -134,10 +163,11 @@ pub trait Database: Sync + Send {
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
Some(old) => {
let new = TPK::from_bytes(&old).unwrap();
let new = new.merge(tpk.clone()).unwrap();
let new = Self::tpk_into_bytes(&new)?;
let tpk = new.merge(tpk.clone()).unwrap();
let new = Self::tpk_into_bytes(&tpk)?;
if self.compare_and_swap(&fpr, Some(&old), Some(&new))? {
self.link_subkeys(&fpr, subkeys)?;
return Ok(ret);
}
}
@ -146,6 +176,7 @@ pub trait Database: Sync + Send {
let fresh = Self::tpk_into_bytes(&tpk)?;
if self.compare_and_swap(&fpr, None, Some(&fresh))? {
self.link_subkeys(&fpr, subkeys)?;
return Ok(ret);
}
}

View File

@ -1,6 +1,6 @@
use errors::Result;
use database::{Verify, Delete, Database, Filesystem, Memory};
use types::{Fingerprint, Email};
use types::{Fingerprint, Email, KeyID};
pub enum Polymorphic {
Memory(Memory),
@ -29,6 +29,34 @@ impl Database for Polymorphic {
}
}
fn link_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
match self {
&Polymorphic::Memory(ref db) => db.link_fpr(from, fpr),
&Polymorphic::Filesystem(ref db) => db.link_fpr(from, fpr),
}
}
fn unlink_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
match self {
&Polymorphic::Memory(ref db) => db.unlink_fpr(from, fpr),
&Polymorphic::Filesystem(ref db) => db.unlink_fpr(from, fpr),
}
}
fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
match self {
&Polymorphic::Memory(ref db) => db.link_kid(kid, fpr),
&Polymorphic::Filesystem(ref db) => db.link_kid(kid, fpr),
}
}
fn unlink_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
match self {
&Polymorphic::Memory(ref db) => db.unlink_kid(kid, fpr),
&Polymorphic::Filesystem(ref db) => db.unlink_kid(kid, fpr),
}
}
fn link_email(&self, email: &Email, fpr: &Fingerprint) {
match self {
&Polymorphic::Memory(ref db) => db.link_email(email, fpr),
@ -70,4 +98,11 @@ impl Database for Polymorphic {
&Polymorphic::Filesystem(ref db) => db.by_email(email),
}
}
fn by_kid(&self, kid: &KeyID) -> Option<Box<[u8]>> {
match self {
&Polymorphic::Memory(ref db) => db.by_kid(kid),
&Polymorphic::Filesystem(ref db) => db.by_kid(kid),
}
}
}

View File

@ -19,8 +19,8 @@ use std::str::FromStr;
use database::Database;
use sequoia_openpgp::tpk::{TPKBuilder, UserIDBinding};
use sequoia_openpgp::{Packet, packet::UserID, TPK, PacketPile};
use types::{Email, Fingerprint};
use sequoia_openpgp::{Packet, packet::UserID, TPK, PacketPile, parse::Parse};
use types::{KeyID, Email, Fingerprint};
pub fn test_uid_verification<D: Database>(db: &mut D) {
let str_uid1 = "Test A <test_a@example.com>";
@ -275,3 +275,45 @@ pub fn test_uid_deletion<D: Database>(db: &mut D) {
assert!(db.by_email(&email1).is_none());
assert!(db.by_email(&email2).is_none());
}
pub fn test_subkey_lookup<D: Database>(db: &mut D) {
let tpk = TPKBuilder::default()
.add_userid("Testy <test@example.com>")
.add_signing_subkey()
.add_encryption_subkey()
.generate().unwrap().0;
// upload key
let _ = db.merge_or_publish(tpk.clone()).unwrap();
let primary_fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
let sub1_fpr = Fingerprint::try_from(tpk.subkeys().next().map(|x| x.subkey().fingerprint()).unwrap()).unwrap();
let sub2_fpr = Fingerprint::try_from(tpk.subkeys().skip(1).next().map(|x| x.subkey().fingerprint()).unwrap()).unwrap();
let raw1 = db.by_fpr(&primary_fpr).unwrap();
let raw2 = db.by_fpr(&sub1_fpr).unwrap();
let raw3 = db.by_fpr(&sub2_fpr).unwrap();
assert_eq!(raw1, raw2);
assert_eq!(raw1, raw3);
}
pub fn test_kid_lookup<D: Database>(db: &mut D) {
let tpk = TPKBuilder::default()
.add_userid("Testy <test@example.com>")
.add_signing_subkey()
.add_encryption_subkey()
.generate().unwrap().0;
// upload key
let _ = db.merge_or_publish(tpk.clone()).unwrap();
let primary_kid = KeyID::try_from(tpk.fingerprint()).unwrap();
let sub1_kid = KeyID::try_from(tpk.subkeys().next().map(|x| x.subkey().fingerprint()).unwrap()).unwrap();
let sub2_kid = KeyID::try_from(tpk.subkeys().skip(1).next().map(|x| x.subkey().fingerprint()).unwrap()).unwrap();
let raw1 = db.by_kid(&primary_kid).unwrap();
let raw2 = db.by_kid(&sub1_kid).unwrap();
let raw3 = db.by_kid(&sub2_kid).unwrap();
assert_eq!(raw1, raw2);
assert_eq!(raw1, raw3);
}

View File

@ -10,8 +10,7 @@ extern crate time;
extern crate url;
extern crate hex;
#[cfg(not(test))] #[macro_use] extern crate rocket;
#[cfg(test)] #[macro_use] extern crate rocket;
#[macro_use] extern crate rocket;
extern crate rocket_contrib;
extern crate multipart;

View File

@ -74,3 +74,52 @@ impl FromStr for Fingerprint {
}
}
}
#[derive(Serialize,Deserialize,Clone,Debug,Hash,PartialEq,Eq)]
pub struct KeyID([u8; 8]);
impl TryFrom<sequoia_openpgp::Fingerprint> for KeyID {
type Error = Error;
fn try_from(fpr: sequoia_openpgp::Fingerprint) -> Result<Self> {
match fpr {
sequoia_openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a).into()),
sequoia_openpgp::Fingerprint::Invalid(_) => Err("invalid fingerprint".into()),
}
}
}
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 ToString for KeyID {
fn to_string(&self) -> String {
format!("0x{}", hex::encode(&self.0[..]))
}
}
impl FromStr for KeyID {
type Err = Error;
fn from_str(s: &str) -> Result<KeyID> {
if !s.starts_with("0x") || s.len() != 16 + 2 {
return Err(format!("'{}' is not a valid long key ID", s).into());
}
let vec = hex::decode(&s[2..])?;
if vec.len() == 8 {
let mut arr = [0u8; 8];
arr.copy_from_slice(&vec[..]);
Ok(KeyID(arr))
} else {
Err(format!("'{}' is not a valid long key ID", s).into())
}
}
}

View File

@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
mod upload;
use database::{Polymorphic, Database};
use types::{Fingerprint, Email};
use types::{Fingerprint, Email, KeyID};
use errors::Result;
use Opt;
@ -140,6 +140,21 @@ fn by_email(db: rocket::State<Polymorphic>, email: String)
}
}
#[get("/by-kid/<kid>")]
fn by_kid(db: rocket::State<Polymorphic>, kid: String)
-> result::Result<String, Custom<String>>
{
let maybe_key = match KeyID::from_str(&kid) {
Ok(ref key) => db.by_kid(key),
Err(_) => None,
};
match maybe_key {
Some(ref bytes) => process_key(bytes),
None => Ok("No such key :-(".to_string()),
}
}
#[get("/vks/verify/<token>")]
fn verify(db: rocket::State<Polymorphic>, token: String)
-> result::Result<Template, Custom<String>>
@ -305,6 +320,7 @@ pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
// nginx-supported lookup
by_email,
by_fpr,
by_kid,
// HKP
lookup,
upload::multipart_upload,