initial commit

This commit is contained in:
seu 2018-08-16 20:35:19 +02:00
commit 66fef4a275
10 changed files with 1195 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "garbage-pile"
version = "0.1.0"
authors = ["seu <seu@panopticon.re>"]
[dependencies]
rocket = "0"
rocket_codegen = "0"
openpgp = { path = "../sequoia/openpgp" }
multipart = "0"
error-chain = "0"
log = "0"
rand = "0.5"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
base64 = "0.9"
time = "0.1"
tempfile = "3.0"
parking_lot = "0.6"
structopt = "0.2"

4
README.md Normal file
View File

@ -0,0 +1,4 @@
Garbage Pile - The verifying OpenPGP key server
===============================================
`cargo run -- --help`

249
src/database/fs.rs Normal file
View File

@ -0,0 +1,249 @@
use std::str;
use std::path::PathBuf;
use std::fs::{File, remove_file, create_dir_all, read_link};
use std::io::{Write, Read};
use std::os::unix::fs::symlink;
use tempfile;
use serde_json;
use openpgp::UserID;
use base64;
use database::{Verify, Delete, Fingerprint, Database};
use Result;
struct Filesystem {
base: PathBuf,
}
impl Filesystem {
pub fn new<P: Into<PathBuf>>(base: P) -> Result<Self> {
use std::fs;
let base: PathBuf = base.into();
if fs::create_dir(&base).is_err() {
let meta = fs::metadata(&base);
match meta {
Ok(meta) => {
if !meta.file_type().is_dir() {
return Err(format!("'{}' exists already and is not a directory",
base.display()).into());
}
if meta.permissions().readonly() {
return Err(format!("Cannot write '{}'", base.display()).into());
}
}
Err(e) => {
return Err(format!("Cannot read '{}': {}", base.display(),e).into());
}
}
}
// create directories
create_dir_all(base.join("verification_tokens"))?;
create_dir_all(base.join("deletion_tokens"))?;
create_dir_all(base.join("scratch_pad"))?;
create_dir_all(base.join("public").join("by-fpr"))?;
create_dir_all(base.join("public").join("by-uid"))?;
info!("Opened base dir '{}'", base.display());
Ok(Filesystem{
base: base,
})
}
fn new_token<'a>(&self, base: &'a str) -> Result<(File, String)> {
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;
let mut rng = thread_rng();
// samples from [a-zA-Z0-9]
// 43 chars ~ 256 bit
let name: String = rng.sample_iter(&Alphanumeric).take(43).collect();
let dir = self.base.join(base);
let fd = File::create(dir.join(name.clone()))?;
Ok((fd, name))
}
fn pop_token<'a>(&self, base: &'a str, token: &'a str) -> Result<Box<[u8]>> {
let path = self.base.join(base).join(token);
let buf = {
let mut fd = File::open(path.clone())?;
let mut buf = Vec::default();
fd.read_to_end(&mut buf)?;
buf.into_boxed_slice()
};
remove_file(path)?;
Ok(buf)
}
}
impl Database for Filesystem {
fn new_verify_token(&mut self, payload: Verify) -> Result<String> {
let (mut fd, name) = self.new_token("verification_tokens")?;
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
Ok(name)
}
fn new_delete_token(&mut self, payload: Delete) -> Result<String> {
let (mut fd, name) = self.new_token("deletion_tokens")?;
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
Ok(name)
}
fn compare_and_swap(&mut self, fpr: &Fingerprint, old: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool> {
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
let dir = self.base.join("scratch_pad");
match new {
Some(new) => {
let mut tmp = tempfile::Builder::new()
.prefix("key")
.rand_bytes(16)
.tempfile_in(dir)?;
tmp.write_all(new)?;
if target.is_file() {
if old.is_some() { remove_file(target.clone())?; }
else { return Err(format!("stray file {}", target.display()).into()); }
}
let _ = tmp.persist(target)?;
Ok(true)
}
None => {
remove_file(target)?;
Ok(true)
}
}
}
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
let link = self.base.join("public").join("by-uid").join(uid);
if link.exists() {
let _ = remove_file(link.clone());
}
let _ = symlink(target, link);
}
fn unlink_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
let link = self.base.join("public").join("by-uid").join(uid);
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(&mut 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())
}).and_then(|s| {
let s = serde_json::from_str(&s);
s.ok()
})
}
fn pop_delete_token(&mut self, token: &str) -> Option<Delete> {
self.pop_token("deletion_tokens", token).ok().and_then(|raw| {
str::from_utf8(&raw).ok().map(|s| s.to_string())
}).and_then(|s| {
serde_json::from_str(&s).ok()
})
}
// XXX: slow
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
File::open(target).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
}
})
}
// XXX: slow
fn by_uid(&self, uid: &str) -> Option<Box<[u8]>> {
let target = self.base.join("public").join("by-uid").join(uid);
File::open(target).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)]
mod tests {
use super::*;
use tempfile::TempDir;
use openpgp::tpk::TPKBuilder;
use database::test;
#[test]
fn init() {
let tmpdir = TempDir::new().unwrap();
let _ = Filesystem::new(tmpdir.path()).unwrap();
}
#[test]
fn new() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new(tmpdir.path()).unwrap();
let k1 = TPKBuilder::default().add_userid("a").generate().unwrap();
let k2 = TPKBuilder::default().add_userid("b").generate().unwrap();
let k3 = TPKBuilder::default().add_userid("c").generate().unwrap();
assert!(db.merge_or_publish(k1).unwrap().len() > 0);
assert!(db.merge_or_publish(k2.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k2).unwrap().len() > 0);
assert!(db.merge_or_publish(k3.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k3.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k3).unwrap().len() > 0);
}
#[test]
fn uid_verification() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new(tmpdir.path()).unwrap();
test::test_uid_verification(&mut db);
}
#[test]
fn uid_deletion() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new(tmpdir.path()).unwrap();
test::test_uid_deletion(&mut db);
}
}

130
src/database/memory.rs Normal file
View File

@ -0,0 +1,130 @@
use std::collections::HashMap;
use parking_lot::Mutex;
use openpgp::UserID;
use base64;
use database::{Verify, Delete, Fingerprint, Database};
use Result;
struct InMemory {
fpr: Mutex<HashMap<Fingerprint, Box<[u8]>>>,
userid: Mutex<HashMap<String, Fingerprint>>,
verify_token: Mutex<HashMap<String, Verify>>,
delete_token: Mutex<HashMap<String, Delete>>,
}
impl Default for InMemory {
fn default() -> Self {
InMemory{
fpr: Mutex::new(HashMap::default()),
userid: Mutex::new(HashMap::default()),
verify_token: Mutex::new(HashMap::default()),
delete_token: Mutex::new(HashMap::default()),
}
}
}
impl Database for InMemory {
fn new_verify_token(&mut self, payload: Verify) -> Result<String> {
let token = Self::new_token();
self.verify_token.lock().insert(token.clone(), payload);
Ok(token)
}
fn new_delete_token(&mut self, payload: Delete) -> Result<String> {
let token = Self::new_token();
self.delete_token.lock().insert(token.clone(), payload);
Ok(token)
}
fn compare_and_swap(&mut self, fpr: &Fingerprint, present: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool> {
let mut fprs = self.fpr.lock();
if fprs.get(fpr).map(|x| &x[..]) == present {
if let Some(new) = new {
fprs.insert(fpr.clone(), new.into());
} else {
fprs.remove(fpr);
}
Ok(true)
} else {
Ok(false)
}
}
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint) {
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
self.userid.lock().insert(uid.to_string(), fpr.clone());
}
fn unlink_userid(&mut self, uid: &UserID, _: &Fingerprint) {
let uid = base64::encode_config(&uid.value, base64::URL_SAFE);
self.userid.lock().remove(&uid.to_string());
}
// (verified uid, fpr)
fn pop_verify_token(&mut self, token: &str) -> Option<Verify> {
self.verify_token.lock().remove(token)
}
// fpr
fn pop_delete_token(&mut self, token: &str) -> Option<Delete> {
self.delete_token.lock().remove(token)
}
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
self.fpr.lock().get(fpr).map(|x| x.clone())
}
fn by_uid(&self, uid: &str) -> Option<Box<[u8]>> {
let userid = self.userid.lock();
let fprs = self.fpr.lock();
userid.get(uid).and_then(|fpr| fprs.get(fpr).map(|x| x.clone()))
}
}
impl InMemory {
pub fn new_token() -> String {
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;
let mut rng = thread_rng();
// samples from [a-zA-Z0-9]
// 43 chars ~ 256 bit
rng.sample_iter(&Alphanumeric).take(43).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use openpgp::tpk::TPKBuilder;
use database::test;
#[test]
fn new() {
let mut db = InMemory::default();
let k1 = TPKBuilder::default().add_userid("a").generate().unwrap();
let k2 = TPKBuilder::default().add_userid("b").generate().unwrap();
let k3 = TPKBuilder::default().add_userid("c").generate().unwrap();
assert!(db.merge_or_publish(k1).unwrap().len() > 0);
assert!(db.merge_or_publish(k2.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k2).unwrap().len() > 0);
assert!(db.merge_or_publish(k3.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k3.clone()).unwrap().len() > 0);
assert!(!db.merge_or_publish(k3).unwrap().len() > 0);
}
#[test]
fn uid_verification() {
let mut db = InMemory::default();
test::test_uid_verification(&mut db);
}
}

305
src/database/mod.rs Normal file
View File

@ -0,0 +1,305 @@
use std::result;
use std::io::Cursor;
use std::str::FromStr;
use std::convert::TryFrom;
use std::fmt;
use serde::{Serializer, Deserializer, de};
use time;
use openpgp::{self, TPK, UserID, Packet, PacketPile, Signature, constants::SignatureType};
use base64;
use {Error, Result};
mod fs;
mod memory;
mod test;
#[derive(Serialize,Deserialize,Clone,Debug,Hash,PartialEq,Eq)]
pub struct Fingerprint([u8; 20]);
impl TryFrom<openpgp::Fingerprint> for Fingerprint {
type Error = Error;
fn try_from(fpr: openpgp::Fingerprint) -> Result<Self> {
match fpr {
openpgp::Fingerprint::V4(a) => Ok(Fingerprint(a)),
openpgp::Fingerprint::Invalid(_) => Err("invalid fingerprint".into()),
}
}
}
impl ToString for Fingerprint {
fn to_string(&self) -> String {
base64::encode_config(&self.0[..], base64::URL_SAFE)
}
}
impl FromStr for Fingerprint {
type Err = Error;
fn from_str(s: &str) -> Result<Fingerprint> {
let vec = base64::decode(s)?;
if vec.len() == 20 {
let mut arr = [0u8; 20];
arr.copy_from_slice(&vec[..]);
Ok(Fingerprint(arr))
} else {
Err(format!("'{}' is not a valid fingerprint", s).into())
}
}
}
#[derive(Serialize,Deserialize,Clone,Debug)]
pub struct Verify {
created: i64,
packets: Box<[u8]>,
fpr: Fingerprint,
#[serde(deserialize_with = "Verify::deserialize_userid", serialize_with = "Verify::serialize_userid")]
uid: UserID,
}
impl Verify {
pub fn new(uid: &UserID, sig: &[&Signature], fpr: Fingerprint) -> Result<Self> {
use openpgp::serialize::Serialize;
let mut cur = Cursor::new(Vec::default());
let res: Result<()> = uid.serialize(&mut cur)
.map_err(|e| format!("openpgp: {}", e).into());
res?;
for s in sig {
let res: Result<()> = s.serialize(&mut cur)
.map_err(|e| format!("openpgp: {}", e).into());
res?;
}
Ok(Verify{
created: time::now().to_timespec().sec,
packets: cur.into_inner().into(),
fpr: fpr,
uid: uid.clone(),
})
}
fn deserialize_userid<'de, D>(de: D) -> result::Result<UserID, D::Error> where D: Deserializer<'de> {
de.deserialize_bytes(UserIDVisitor)
}
fn serialize_userid<S>(uid: &UserID, ser: S) -> result::Result<S::Ok, S::Error> where S: Serializer {
ser.serialize_bytes(&uid.value)
}
}
struct UserIDVisitor;
impl<'de> de::Visitor<'de> for UserIDVisitor {
type Value = UserID;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a OpenPGP User ID")
}
fn visit_bytes<E>(self, s: &[u8]) -> result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(UserID::new().userid_from_bytes(s))
}
fn visit_seq<A>(self, mut seq: A) -> result::Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>
{
let mut buf = Vec::default();
while let Some(x) = seq.next_element()? {
buf.push(x);
}
Ok(UserID::new().userid_from_bytes(&buf))
}
}
#[derive(Serialize,Deserialize,Clone,Debug)]
pub struct Delete {
created: i64,
fpr: Fingerprint
}
impl Delete {
pub fn new(fpr: Fingerprint) -> Self {
Delete{
created: time::now().to_timespec().sec,
fpr: fpr
}
}
}
// uid -> uidsig+
// subkey -> subkeysig+
pub trait Database: Sync + Send{
fn new_verify_token(&mut self, payload: Verify) -> Result<String>;
fn new_delete_token(&mut self, payload: Delete) -> Result<String>;
fn compare_and_swap(&mut self, fpr: &Fingerprint, present: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool>;
fn link_userid(&mut self, uid: &UserID, fpr: &Fingerprint);
fn unlink_userid(&mut self, uid: &UserID, fpr: &Fingerprint);
// (verified uid, fpr)
fn pop_verify_token(&mut self, token: &str) -> Option<Verify>;
// fpr
fn pop_delete_token(&mut self, token: &str) -> Option<Delete>;
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>>;
fn by_uid(&self, uid: &str) -> 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) => sig.sigtype == SignatureType::DirectKey,
_ => false,
}
}).collect::<Vec<_>>();
TPK::from_packet_pile(PacketPile::from_packets(pile))
.map_err(|e| format!("openpgp: {}", e).into())
}
fn tpk_into_bytes(tpk: &TPK) -> Result<Vec<u8>> {
use std::io::Cursor;
let mut cur = Cursor::new(Vec::default());
tpk.serialize(&mut cur).map(|_| cur.into_inner()).map_err(|e| format!("{}", e).into())
}
fn merge_or_publish(&mut self, mut tpk: TPK) -> Result<Vec<String>> {
let fpr = Fingerprint::try_from(tpk.primary().fingerprint())?;
let mut ret = Vec::default();
// update verify tokens
for uid in tpk.userids() {
let enc = base64::encode_config(&format!("{}", uid.userid()), base64::URL_SAFE);
if self.by_uid(&enc).is_none() {
let payload = Verify::new(uid.userid(), &uid.selfsigs().collect::<Vec<_>>(), fpr.clone())?;
// XXX: send mail
ret.push(self.new_verify_token(payload)?);
}
}
tpk = Self::strip_userids(tpk)?;
for _ in 0..100 /* while cas failed */ {
// merge or update key db
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)?;
if self.compare_and_swap(&fpr, Some(&old), Some(&new))? {
return Ok(ret);
}
}
None => {
let fresh = Self::tpk_into_bytes(&tpk)?;
if self.compare_and_swap(&fpr, None, Some(&fresh))? {
return Ok(ret);
}
}
}
}
error!("Compare-and-swap of {} failed {} times in a row. Aborting.", fpr.to_string(), 100);
Err("Database update failed".into())
}
// if (uid, fpr) = pop-token(tok) {
// while cas-failed() {
// tpk = by_fpr(fpr)
// merged = add-uid(tpk, uid)
// cas(tpk, merged)
// }
// }
fn verify_token(&mut self, token: &str) -> Result<bool> {
match self.pop_verify_token(token) {
Some(Verify{ created, packets, fpr, uid }) => {
let now = time::now().to_timespec().sec;
if created > now || now - created > 3 * 3600 { return Ok(false); }
loop /* while cas falied */ {
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
Some(old) => {
let mut new = old.clone();
new.extend(packets.into_iter());
if self.compare_and_swap(&fpr, Some(&old), Some(&new))? {
self.link_userid(&uid, &fpr);
return Ok(true);
}
}
None => {
return Ok(false);
}
}
}
}
None => Err("No such token".into()),
}
}
fn request_deletion(&mut self, fpr: Fingerprint) -> Result<String> {
if self.by_fpr(&fpr).is_none() { return Err("Unknown key".into()); }
let payload = Delete::new(fpr);
self.new_delete_token(payload)
}
// if fpr = pop-token(tok) {
// tpk = by_fpr(fpr)
// for uid in tpk.userids {
// del-uid(uid)
// }
// del-fpr(fpr)
// }
fn confirm_deletion(&mut self, token: &str) -> Result<bool> {
match self.pop_delete_token(token) {
Some(Delete{ created, fpr }) => {
let now = time::now().to_timespec().sec;
if created > now || now - created > 3 * 3600 { return Ok(false); }
loop {
match self.by_fpr(&fpr).map(|x| x.to_vec()) {
Some(old) => {
let tpk = match TPK::from_bytes(&old) {
Ok(tpk) => tpk,
Err(e) => {
return Err(format!("Failed to parse old TPK: {:?}", e).into());
}
};
for uid in tpk.userids() {
self.unlink_userid(uid.userid(), &fpr);
}
while !self.compare_and_swap(&fpr, Some(&old), None)? {}
return Ok(true);
}
None => {
return Ok(false);
}
}
}
}
None => Ok(false),
}
}
}

268
src/database/test.rs Normal file
View File

@ -0,0 +1,268 @@
// pub, fetch by fpr, verify no uid
// verify uid fetch by fpr fetch by uid
// verify again
// verify other uid fetch by ui1 uid2 fpr
// pub again
// pub with less uid
// pub with new uid
//
// pub & verify
// req del one
// fetch by uid & fpr
// confirm
// fetch by uid & fpr
// confirm again
// fetch by uid & fpr
use std::convert::TryFrom;
use std::thread;
use std::sync::atomic::{Ordering, AtomicBool};
use database::{Fingerprint, Database};
use openpgp::tpk::{TPKBuilder, UserIDBinding};
use openpgp::{UserID, TPK, PacketPile, Packet};
use base64;
pub fn test_uid_verification<D: Database>(db: &mut D) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let tpk = TPKBuilder::default()
.add_userid(str_uid1)
.add_userid(str_uid2)
.generate().unwrap();
let uid1 = UserID::new().userid_from_bytes(str_uid1.as_bytes());
let uid2 = UserID::new().userid_from_bytes(str_uid2.as_bytes());
let b64_uid1 = base64::encode_config(str_uid1, base64::URL_SAFE);
let b64_uid2 = base64::encode_config(str_uid2, base64::URL_SAFE);
// upload key
let tokens = db.merge_or_publish(tpk.clone()).unwrap();
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
assert_eq!(tokens.len(), 2);
{
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert!(key.userids().next().is_none());
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
}
// fail to fetch by uid
assert!(db.by_uid(&b64_uid1).is_none());
assert!(db.by_uid(&b64_uid2).is_none());
// verify 1st uid
assert!(db.verify_token(&tokens[0]).unwrap());
{
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert!(key.userids().skip(1).next().is_none());
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let uid = key.userids().next().unwrap().userid().clone();
assert!((uid == uid1) ^ (uid == uid2));
let b64_uid = base64::encode_config(&String::from_utf8(uid.value.clone()).unwrap(), base64::URL_SAFE);
assert_eq!(db.by_uid(&b64_uid).unwrap(), raw);
if b64_uid1 == b64_uid {
assert!(db.by_uid(&b64_uid2).is_none());
} else if b64_uid2 == b64_uid {
assert!(db.by_uid(&b64_uid1).is_none());
} else {
unreachable!()
}
}
// verify 1st uid again
assert!(db.verify_token(&tokens[0]).is_err());
{
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert!(key.userids().skip(1).next().is_none());
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let uid = key.userids().next().unwrap().userid().clone();
assert!((uid == uid1) ^ (uid == uid2));
let b64_uid = base64::encode_config(&String::from_utf8(uid.value.clone()).unwrap(), base64::URL_SAFE);
assert_eq!(db.by_uid(&b64_uid).unwrap(), raw);
if b64_uid1 == b64_uid {
assert!(db.by_uid(&b64_uid2).is_none());
} else if b64_uid2 == b64_uid {
assert!(db.by_uid(&b64_uid1).is_none());
} else {
unreachable!()
}
}
// verify 2nd uid
assert!(db.verify_token(&tokens[1]).unwrap());
{
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert_eq!(key.userids().len(), 2);
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let myuid1 = key.userids().next().unwrap().userid().clone();
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
}
// upload again
assert_eq!(db.merge_or_publish(tpk.clone()).unwrap(), Vec::<String>::default());
// publish w/ one uid less
{
let packets = tpk.clone()
.to_packet_pile()
.into_children()
.filter(|pkt| {
match pkt {
Packet::UserID(ref uid) => *uid != uid1,
_ => true
}
});
let pile = PacketPile::from_packets(packets.collect());
let short_tpk = TPK::from_packet_pile(pile).unwrap();
assert_eq!(db.merge_or_publish(short_tpk.clone()).unwrap(), Vec::<String>::default());
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert_eq!(key.userids().len(), 2);
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let myuid1 = key.userids().next().unwrap().userid().clone();
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
}
// publish w/one uid more
{
let mut packets = tpk.clone()
.to_packet_pile()
.into_children()
.filter(|pkt| {
match pkt {
Packet::UserID(ref uid) => *uid != uid1,
_ => true
}
}).collect::<Vec<_>>();
let str_uid3 = "Test C <test_c@example.com>";
let uid3 = UserID::new().userid_from_bytes(str_uid3.as_bytes());
let b64_uid3 = base64::encode_config(str_uid3, base64::URL_SAFE);
let key = tpk.primary();
let bind = UserIDBinding::new(key, uid3.clone(), key).unwrap();
packets.push(Packet::UserID(uid3.clone()));
packets.push(Packet::Signature(bind.selfsigs().next().unwrap().clone()));
let pile = PacketPile::from_packets(packets);
let ext_tpk = TPK::from_packet_pile(pile).unwrap();
let tokens = db.merge_or_publish(ext_tpk.clone()).unwrap();
assert_eq!(tokens.len(), 1);
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert_eq!(key.userids().len(), 2);
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let myuid1 = key.userids().next().unwrap().userid().clone();
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
assert!(db.by_uid(&b64_uid3).is_none());
}
}
pub fn test_uid_deletion<D: Database>(db: &mut D) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let tpk = TPKBuilder::default()
.add_userid(str_uid1)
.add_userid(str_uid2)
.generate().unwrap();
let uid1 = UserID::new().userid_from_bytes(str_uid1.as_bytes());
let uid2 = UserID::new().userid_from_bytes(str_uid2.as_bytes());
let b64_uid1 = base64::encode_config(str_uid1, base64::URL_SAFE);
let b64_uid2 = base64::encode_config(str_uid2, base64::URL_SAFE);
// upload key and verify uids
let tokens = db.merge_or_publish(tpk.clone()).unwrap();
assert_eq!(tokens.len(), 2);
assert!(db.verify_token(&tokens[0]).unwrap());
assert!(db.verify_token(&tokens[1]).unwrap());
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
// req. deletion
let del = db.request_deletion(fpr.clone()).unwrap();
// check it's still there
{
// fetch by fpr
let raw = db.by_fpr(&fpr).unwrap();
let key = TPK::from_bytes(&raw[..]).unwrap();
assert_eq!(key.userids().len(), 2);
assert!(key.user_attributes().next().is_none());
assert!(key.subkeys().next().is_none());
let myuid1 = key.userids().next().unwrap().userid().clone();
let myuid2 = key.userids().skip(1).next().unwrap().userid().clone();
assert_eq!(db.by_uid(&b64_uid1).unwrap(), raw);
assert_eq!(db.by_uid(&b64_uid2).unwrap(), raw);
assert!(((myuid1 == uid1) & (myuid2 == uid2)) ^ ((myuid1 == uid2) & (myuid2 == uid1)));
}
// confirm deletion
assert!(db.confirm_deletion(&del).unwrap());
// check it's gone
assert!(db.by_fpr(&fpr).is_none());
assert!(db.by_uid(&b64_uid1).is_none());
assert!(db.by_uid(&b64_uid2).is_none());
// confirm deletion again
assert!(!db.confirm_deletion(&del).unwrap());
// check it's still gone
assert!(db.by_fpr(&fpr).is_none());
assert!(db.by_uid(&b64_uid1).is_none());
assert!(db.by_uid(&b64_uid2).is_none());
}

63
src/main.rs Normal file
View File

@ -0,0 +1,63 @@
#![feature(plugin, decl_macro, custom_derive)]
#![plugin(rocket_codegen)]
#![recursion_limit = "1024"]
#![feature(try_from)]
extern crate serde;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
extern crate time;
extern crate base64;
#[cfg(not(test))] #[macro_use] extern crate rocket;
#[cfg(test)] extern crate rocket;
extern crate openpgp;
extern crate multipart;
#[macro_use] extern crate error_chain;
#[macro_use] extern crate log;
extern crate rand;
extern crate tempfile;
extern crate parking_lot;
#[macro_use] extern crate structopt;
mod web;
mod database;
mod errors {
error_chain!{
foreign_links {
Fmt(::std::fmt::Error);
Io(::std::io::Error);
Json(::serde_json::Error);
Persist(::tempfile::PersistError);
Base64(::base64::DecodeError);
}
}
}
use errors::*;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(name = "garbage", about = "Garbage Pile - The verifying OpenPGP key server.")]
struct Opt {
/// Debug mode
#[structopt(short = "v", long = "verbose")]
debug: bool,
/// Daemon
#[structopt(short = "d", long = "daemon")]
daemon: bool,
/// Base directory
#[structopt(parse(from_os_str))]
base: PathBuf,
/// Listen
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:80")]
listen: String,
}
fn main() {
let opt = Opt::from_args();
println!("{:?}", opt);
}

75
src/web/mod.rs Normal file
View File

@ -0,0 +1,75 @@
use rocket;
use rocket::response::content;
mod upload;
#[get("/key/<fpr>")]
fn key_by_fingerprint(fpr: String) -> String {
format!("{}", fpr)
}
#[derive(FromForm)]
struct KeySubmissionForm {
key: String,
}
#[post("/keys", data = "<data>")]
fn submit_key(data: String) -> String {
//use multipart::server::Multipart;
//use openpgp::TPK;
format!("{:?}", data)/*
let strm = form.open();
match TPK::from_reader(strm) {
Ok(tpk) => {
match tpk.userids().next() {
Some(uid) => {
format!("Hello, {:?}", uid.userid())
}
None => {
format!("Hello, {:?}", tpk.primary().fingerprint())
}
}
}
Err(e) => {
format!("Error: {:?}", e)
}
}*/
}
#[get("/")]
fn root() -> content::Html<&'static str> {
content::Html("
<!doctype html>
<html>
<head>
<title>Garbage Pile Public Key Server</title>
</head>
<body>
<h1>Garbage Pile Public Key Server</h1>
<p>The verifying PGP key server. Powered by p&equiv;pnology!
<h2>Search for keys</h2>
<form action=\"/search\" method=POST>
<input type=\"search\" id=\"query\" name=\"query\" placeholder=\"Email\">
<input type=\"submit\" value=\"Search\">
</form>
<h2>Upload your key</h2>
<form action=\"/keys\" method=POST enctype=multipart/form-data>
<input type=\"file\" id=\"key\" name=\"key\" placeholder=\"Your public key\">
<input type=\"submit\" value=\"Upload\">
</form>
</body>
</html>")
}
fn main() {
rocket::ignite().mount("/", routes![
upload::multipart_upload,
key_by_fingerprint,
root]).launch();
}
//POST /keys
//GET /keys/<fpr>

77
src/web/upload.rs Normal file
View File

@ -0,0 +1,77 @@
use multipart::server::Multipart;
use multipart::server::save::Entries;
use multipart::server::save::SaveResult::*;
use rocket::Data;
use rocket::http::{ContentType, Status};
use rocket::response::status::Custom;
#[post("/keys", data = "<data>")]
// signature requires the request to have a `Content-Type`
pub fn multipart_upload(cont_type: &ContentType, data: Data) -> Result<String, Custom<String>> {
// this and the next check can be implemented as a request guard but it seems like just
// more boilerplate than necessary
if !cont_type.is_form_data() {
return Err(Custom(
Status::BadRequest,
"Content-Type not multipart/form-data".into()
));
}
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|| Custom(
Status::BadRequest,
"`Content-Type: multipart/form-data` boundary param not provided".into()
)
)?;
process_upload(boundary, data)
}
fn process_upload(boundary: &str, data: Data) -> Result<String, Custom<String>> {
// saves all fields, any field longer than 10kB goes to a temporary directory
// Entries could implement FromData though that would give zero control over
// how the files are saved; Multipart would be a good impl candidate though
match Multipart::with_body(data.open(), boundary).save().temp() {
Full(entries) => process_entries(entries),
Partial(partial, _) => {
process_entries(partial.entries)
},
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
}
}
// having a streaming output would be nice; there's one for returning a `Read` impl
// but not one that you can `write()` to
fn process_entries(entries: Entries) -> Result<String, Custom<String>> {
use openpgp::TPK;
match entries.fields.get(&"key".to_string()) {
Some(ent) if ent.len() == 1 => {
let reader = ent[0].data.readable().map_err(|err| {
Custom(Status::InternalServerError, err.to_string())
})?;
match TPK::from_reader(reader) {
Ok(tpk) => {
match tpk.userids().next() {
Some(uid) => {
Ok(format!("Hello, {:?}", uid.userid()))
}
None => {
Ok(format!("Hello, {:?}", tpk.primary().fingerprint()))
}
}
}
Err(e) => {
Ok(format!("Error: {:?}", e))
}
}
}
Some(_) | None => Err(Custom(
Status::BadRequest,
"Not a PGP public key".into()
)),
}
}