Add sq wot

- Add the `sq wot` subcommand, to expose web of trust functionality.

  - This is just an import of the `sq-wot` CLI as `sq wot`.  The
    support for using the `gpg` keyring and gpg's ownertrust, however,
    is removed.
This commit is contained in:
Neal H. Walfield 2023-03-27 22:54:23 +02:00
parent 8cf08e2470
commit 47447cd7d0
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
25 changed files with 7775 additions and 151 deletions

1
Cargo.lock generated
View File

@ -2873,6 +2873,7 @@ dependencies = [
"clap 4.0.32",
"clap_complete",
"dirs",
"dot-writer",
"fehler",
"itertools 0.10.3",
"once_cell",

View File

@ -31,6 +31,7 @@ maintenance = { status = "actively-developed" }
[dependencies]
buffered-reader = { version = "1.0.0", default-features = false, features = ["compression-deflate"] }
dirs = "4"
dot-writer = "0.1.3"
sequoia-openpgp = { version = "1.13", default-features = false, features = ["compression-deflate"] }
sequoia-autocrypt = { version = "0.25", default-features = false, optional = true }
sequoia-net = { version = "0.26", default-features = false }

7
NEWS
View File

@ -51,6 +51,13 @@
local trust root on the specified bindings.
- Add a top-level option, `--keyring`, to allow the user to specify
additional keyrings to search for certificates.
- Import web of trust subcommands from sq-wot. Specifically, add:
- `sq wot authenticate` to authenticate a binding.
- `sq wot lookup` to find a certificate with a particular User ID.
- `sq wot identify` to list authenticated bindings for a
certificate.
- `sq wot list` to list authenticated bindings.
- `sq wot path` to authenticate and lint a path in a web of trust.
* Deprecated functionality
- `sq key generate --creation-time TIME` is deprecated in favor of
`sq key generate --time TIME`.

View File

@ -35,8 +35,7 @@ use openpgp::types::RevocationStatus;
use sequoia_cert_store as cert_store;
use cert_store::Store;
use sequoia_wot as wot;
use wot::store::Store as _;
use sequoia_wot::store::Store as _;
use crate::{
Config,
@ -66,8 +65,7 @@ pub mod export;
pub mod net;
pub mod certify;
pub mod link;
use crate::error_chain;
pub mod wot;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GetKeysOptions {
@ -451,129 +449,6 @@ pub fn encrypt(opts: EncryptOpts) -> Result<()> {
Ok(())
}
// Prints the path in the web of trust.
fn print_path(path: &wot::PathLints, target_userid: &UserID, prefix: &str)
{
let certification_count = path.certifications().count();
eprint!("{}{}", prefix, path.root().key_handle());
if certification_count == 0 {
eprint!(" {:?}", String::from_utf8_lossy(target_userid.value()));
} else if let Some(userid) = path.root().primary_userid() {
eprint!(" ({:?})",
String::from_utf8_lossy(userid.value()));
}
eprintln!("");
for (last, (cert, certification)) in path
.certs()
.zip(path.certifications())
.enumerate()
.map(|(j, c)| {
if j + 1 == certification_count {
(true, c)
} else {
(false, c)
}
})
{
eprint!("{}", prefix);
if let Some(certification) = certification.certification() {
if certification.amount() < wot::FULLY_TRUSTED {
eprint!(" partially certified (amount: {} of {})",
certification.amount(), wot::FULLY_TRUSTED);
} else {
eprint!(" certified");
}
if last {
eprint!(" the following binding");
} else {
eprint!(" the following certificate");
}
eprint!(" on {}",
chrono::DateTime::<chrono::Utc>::from(
certification.creation_time()).format("%Y-%m-%d"));
if let Some(e) = certification.expiration_time() {
eprint!(" (expiry: {})",
chrono::DateTime::<chrono::Utc>::from(
e).format("%Y-%m-%d"));
}
if certification.depth() > 0.into() {
eprint!(" as a");
if certification.amount() != wot::FULLY_TRUSTED {
eprint!(" partially trusted ({} of {})",
certification.amount(), wot::FULLY_TRUSTED);
} else {
eprint!(" fully trusted");
}
if certification.depth() == 1.into() {
eprint!(" introducer (depth: {})",
certification.depth());
} else {
eprint!(" meta-introducer (depth: {})",
certification.depth());
}
}
} else {
eprint!(" No adequate certification found.");
}
eprintln!("");
for err in cert.errors().iter().chain(cert.lints()) {
for (i, msg) in error_chain(err).into_iter().enumerate() {
eprintln!("{}{}{}",
prefix,
if i == 0 { "" } else { " " },
msg);
}
}
for err in certification.errors().iter()
.chain(certification.lints())
{
for (i, msg) in error_chain(err).into_iter().enumerate() {
eprintln!("{}{}{}",
prefix,
if i == 0 { "" } else { " " },
msg);
}
}
eprint!("{}{} {}",
prefix,
if last { "" } else { "" },
certification.target());
if last {
eprint!(" {:?}",
String::from_utf8_lossy(target_userid.value()));
} else {
if let Some(userid) = certification.target_cert()
.and_then(|c| c.primary_userid())
{
eprint!(" ({:?})",
String::from_utf8_lossy(userid.value()));
}
}
eprintln!("");
if last {
let target = path.certs().last().expect("have one");
for err in target.errors().iter().chain(target.lints()) {
for (i, msg) in error_chain(err).into_iter().enumerate() {
eprintln!("{} {}{}",
prefix,
if i == 0 { "" } else { " " },
msg);
}
}
}
}
eprintln!("");
}
struct VHelper<'a, 'store> {
#[allow(dead_code)]
config: &'a Config<'store>,
@ -632,9 +507,11 @@ impl<'a, 'store> VHelper<'a, 'store> {
}
fn print_sigs(&mut self, results: &[VerificationResult]) {
use crate::commands::wot::output::print_path;
use crate::print_error_chain;
let reference_time = self.config.time;
use crate::print_error_chain;
use self::VerificationError::*;
for result in results {
let (sig, ka) = match result {
@ -705,7 +582,7 @@ impl<'a, 'store> VHelper<'a, 'store> {
if let Ok(Some(cert_store)) = self.config.cert_store() {
// Build the network.
let cert_store = wot::store::CertStore::from_store(
let cert_store = sequoia_wot::store::CertStore::from_store(
cert_store, &self.config.policy, reference_time);
let userids = if let Some(userid) = sig.signers_user_id() {
@ -722,9 +599,9 @@ impl<'a, 'store> VHelper<'a, 'store> {
eprintln!("{}{} cannot be authenticated. \
It has no User IDs",
prefix, cert_fpr);
} else if let Ok(n) = wot::Network::new(&cert_store) {
let mut q = wot::QueryBuilder::new(&n);
q.roots(wot::Roots::new(trust_roots.into_iter()));
} else if let Ok(n) = sequoia_wot::Network::new(&cert_store) {
let mut q = sequoia_wot::QueryBuilder::new(&n);
q.roots(sequoia_wot::Roots::new(trust_roots.into_iter()));
let q = q.build();
let authenticated_userids
@ -735,14 +612,15 @@ impl<'a, 'store> VHelper<'a, 'store> {
let paths = q.authenticate(
userid, cert.fingerprint(),
// XXX: Make this user configurable.
wot::FULLY_TRUSTED);
sequoia_wot::FULLY_TRUSTED);
let amount = paths.amount();
let authenticated = if amount >= wot::FULLY_TRUSTED {
let authenticated = if amount >= sequoia_wot::FULLY_TRUSTED {
eprintln!("{}Fully authenticated \
({} of {}) {}, {}",
prefix,
amount, wot::FULLY_TRUSTED,
amount,
sequoia_wot::FULLY_TRUSTED,
cert_fpr,
userid_str);
true
@ -750,7 +628,8 @@ impl<'a, 'store> VHelper<'a, 'store> {
eprintln!("{}Partially authenticated \
({} of {}) {}, {:?} ",
prefix,
amount, wot::FULLY_TRUSTED,
amount,
sequoia_wot::FULLY_TRUSTED,
cert_fpr,
userid_str);
false

552
src/commands/wot.rs Normal file
View File

@ -0,0 +1,552 @@
use anyhow::Context;
use sequoia_openpgp as openpgp;
use openpgp::KeyID;
use openpgp::Fingerprint;
use openpgp::KeyHandle;
use openpgp::Result;
use openpgp::packet::UserID;
use openpgp::policy::Policy;
use sequoia_cert_store as cert_store;
use cert_store::store::StatusListener;
use cert_store::store::StatusUpdate;
use cert_store::store::StoreError;
use sequoia_wot as wot;
use wot::store::CertStore;
pub mod output;
use crate::sq_cli::wot as wot_cli;
use wot_cli::Command;
use crate::commands::wot as wot_cmd;
use wot_cmd::output::print_path;
use wot_cmd::output::print_path_header;
use wot_cmd::output::print_path_error;
use crate::Config;
fn trust_amount(cli: &Command)
-> Result<usize>
{
let amount = if let Some(v) = cli.trust_amount {
v as usize
} else if cli.full {
wot::FULLY_TRUSTED
} else if cli.partial {
wot::PARTIALLY_TRUSTED
} else if cli.double {
2 * wot::FULLY_TRUSTED
} else {
if cli.certification_network {
// Look for multiple paths. Specifically, try to find 10
// paths.
10 * wot::FULLY_TRUSTED
} else {
wot::FULLY_TRUSTED
}
};
Ok(amount)
}
// Returns whether there is a matching self-signed User ID.
fn have_self_signed_userid(cert: &wot::CertSynopsis,
pattern: &UserID, email: bool)
-> bool
{
if email {
if let Ok(Some(pattern)) = pattern.email_normalized() {
// userid contains a valid email address.
cert.userids().any(|u| {
if let Ok(Some(userid)) = u.userid().email_normalized() {
pattern == userid
} else {
false
}
})
} else {
false
}
} else {
cert.userids().any(|u| u.userid() == pattern)
}
}
/// Authenticate bindings defined by a Query on a Network
fn authenticate<S>(
config: &Config,
cli: &Command,
q: &wot::Query<'_, S>,
gossip: bool,
userid: Option<&UserID>,
certificate: Option<&KeyHandle>,
) -> Result<()>
where S: wot::store::Store
{
let required_amount = trust_amount(cli)?;
let fingerprint: Option<Fingerprint> = if let Some(kh) = certificate {
Some(match kh {
KeyHandle::Fingerprint(fpr) => fpr.clone(),
kh @ KeyHandle::KeyID(_) => {
let certs = q.network().lookup_synopses(kh)?;
if certs.is_empty() {
return Err(StoreError::NotFound(kh.clone()).into());
}
if certs.len() > 1 {
return Err(anyhow::anyhow!(
"The Key ID {} is ambiguous. \
It could refer to any of the following \
certificates: {}.",
kh,
certs.into_iter()
.map(|c| c.fingerprint().to_hex())
.collect::<Vec<String>>()
.join(", ")));
}
certs[0].fingerprint()
}
})
} else {
None
};
let email = cli.subcommand.email();
let mut bindings = Vec::new();
if matches!(userid, Some(_)) && email {
let userid = userid.expect("required");
// First, we check that the supplied User ID is a bare
// email address.
let email = String::from_utf8(userid.value().to_vec())
.context("email address must be valid UTF-8")?;
let userid_check = UserID::from(format!("<{}>", email));
if let Ok(Some(email_check)) = userid_check.email() {
if email != email_check {
println!("{:?} does not appear to be an email address",
email);
std::process::exit(1);
}
} else {
println!("{:?} does not appear to be an email address",
email);
std::process::exit(1);
}
// Now, iterate over all of the certifications of the target,
// and select the bindings where the User ID matches the email
// address.
bindings = if let Some(fingerprint) = fingerprint.as_ref() {
q.network().certified_userids_of(fingerprint)
.into_iter()
.map(|userid| (fingerprint.clone(), userid))
.collect::<Vec<_>>()
} else {
q.network().lookup_synopses_by_email(&email)
};
let email_normalized = userid_check.email_normalized()
.expect("checked").expect("checked");
bindings = bindings.into_iter()
.filter_map(|(fingerprint, userid_other)| {
if let Ok(Some(email_other_normalized))
= userid_other.email_normalized()
{
if email_normalized == email_other_normalized {
Some((fingerprint, userid_other.clone()))
} else {
None
}
} else {
None
}
}).collect();
} else if let Some(fingerprint) = fingerprint {
if let Some(userid) = userid {
bindings.push((fingerprint, userid.clone()));
} else {
// Fingerprint, no User ID.
bindings = q.network().certified_userids_of(&fingerprint)
.into_iter()
.map(|userid| (fingerprint.clone(), userid))
.collect();
}
} else if let Some(userid) = userid {
// The caller did not specify a certificate. Find all
// bindings with the User ID.
bindings = q.network().lookup_synopses_by_userid(userid.clone())
.into_iter()
.map(|fpr| (fpr, userid.clone()))
.collect();
} else {
// No User ID, no Fingerprint.
// List everything.
bindings = q.network().certified_userids();
if let wot_cli::Subcommand::List { pattern: Some(pattern), .. } = &cli.subcommand {
// Or rather, just User IDs that match the pattern.
let pattern = pattern.to_lowercase();
bindings = bindings
.into_iter()
.filter(|(_fingerprint, userid)| {
if email {
// Compare with the normalized email address,
// and the raw email address.
if let Ok(Some(email)) = userid.email_normalized() {
// A normalized email is already lowercase.
if email.contains(&pattern) {
return true;
}
}
if let Ok(Some(email)) = userid.email() {
if email.to_lowercase().contains(&pattern) {
return true;
}
}
return false;
} else if let Ok(userid)
= std::str::from_utf8(userid.value())
{
userid.to_lowercase().contains(&pattern)
} else {
// Ignore User IDs with invalid UTF-8.
false
}
})
.collect();
}
};
// There may be multiple certifications of the same
// User ID. Dedup.
bindings.sort();
bindings.dedup();
let mut authenticated = false;
let mut lint_input = true;
let mut output = match config.output_format {
crate::output::OutputFormat::DOT => {
Box::new(output::DotOutputNetwork::new(
required_amount,
q.roots(),
gossip,
cli.certification_network,
))
as Box<dyn output::OutputType>
}
_ => {
Box::new(
output::HumanReadableOutputNetwork::new(required_amount, gossip)
)
}
};
for (fingerprint, userid) in bindings.iter() {
let mut aggregated_amount = 0;
let paths = if gossip {
// Gossip.
let paths = q.gossip(
fingerprint.clone(), userid.clone());
// Sort so the shortest paths come first.
let mut paths: Vec<_> = paths
.into_values()
.map(|(path, _amount)| path)
.collect();
paths.sort_by_key(|path| path.len());
// This means: exit code is 0, which is what we want when
// we've found at least one path.
if paths.len() > 0 {
authenticated = true;
lint_input = false;
}
paths.into_iter()
.map(|p| (p, 0))
.collect::<Vec<(wot::Path, usize)>>()
} else {
let paths = q.authenticate(
userid.clone(), fingerprint.clone(), required_amount);
aggregated_amount = paths.amount();
if aggregated_amount == 0 {
continue;
}
lint_input = false;
if aggregated_amount >= required_amount {
authenticated = true;
}
paths.into_iter().collect::<Vec<(wot::Path, usize)>>()
};
output.add_paths(paths, fingerprint, userid, aggregated_amount)?;
}
output.finalize()?;
// We didn't show anything. Try to figure out what was wrong.
if lint_input {
// See if the target certificate exists.
if let Some(kh) = certificate {
match q.network().lookup_synopses(kh) {
Err(err) => {
eprintln!("Looking up target certificate ({}): {}",
kh, err);
}
Ok(certs) => {
for cert in certs.iter() {
let fpr = cert.fingerprint();
let kh = if certs.len() == 1 {
KeyHandle::KeyID(KeyID::from(&fpr))
} else {
KeyHandle::Fingerprint(fpr.clone())
};
// Check if the certificate was revoke.
use wot::RevocationStatus;
match cert.revocation_status() {
RevocationStatus::Soft(_)
| RevocationStatus::Hard => {
eprintln!("Warning: {} is revoked.", kh);
}
RevocationStatus::NotAsFarAsWeKnow => (),
}
// Check if the certificate has expired.
if let Some(e) = cert.expiration_time() {
if e <= q.network().reference_time() {
eprintln!("Warning: {} is expired.", kh);
}
}
// See if there is a matching self-signed User ID.
if let Some(userid) = userid {
if ! have_self_signed_userid(cert, userid, email) {
eprintln!("Warning: {} is not a \
self-signed User ID for {}.",
userid, kh);
}
}
// See if there are any certifications made on
// this certificate.
if let Ok(cs) = q.network()
.certifications_of(&fpr, 0.into())
{
if cs.iter().all(|cs| {
cs.certifications()
.all(|(_userid, certifications)| {
certifications.is_empty()
})
})
{
eprintln!("Warning: {} has no valid \
certifications.",
kh);
}
}
}
}
}
}
// Perhaps the caller specified an email address, but forgot
// to add --email. If --email is not present and the
// specified User ID looks like an email, try and be helpful.
if ! email {
if let Some(userid) = userid {
if let Ok(email) = std::str::from_utf8(userid.value()) {
let userid_check = UserID::from(format!("<{}>", email));
if let Ok(Some(email_check)) = userid_check.email() {
if email == email_check {
eprintln!("WARNING: {} appears to be a bare \
email address. Perhaps you forgot \
to specify --email.",
email);
}
}
}
}
}
// See if the trust roots exist.
if ! gossip {
if q.roots().iter().all(|r| {
let fpr = r.fingerprint();
if let Err(err) = q.network().lookup_synopsis_by_fpr(&fpr) {
eprintln!("Looking up trust root ({}): {}.",
fpr, err);
true
} else {
false
}
})
{
eprintln!("No trust roots found.");
}
}
}
if ! authenticated {
if ! lint_input {
eprintln!("Could not authenticate any paths.");
} else {
eprintln!("No paths found.");
}
std::process::exit(1);
}
Ok(())
}
// For `sq-wot path`.
fn check_path<'a: 'b, 'b, S>(config: &Config,
cli: &Command, q: &wot::Query<'b, S>,
policy: &dyn Policy)
-> Result<()>
where S: wot::store::Store + wot::store::Backend<'a>
{
tracer!(TRACE, "check_path");
let required_amount = trust_amount(cli)?;
let (khs, userid) = if let wot_cli::Subcommand::Path { path, .. } = &cli.subcommand {
(path.certs()?, path.userid()?)
} else {
unreachable!("checked");
};
assert!(khs.len() > 0, "guaranteed by clap");
let r = q.lint_path(&khs, &userid, required_amount, policy);
let target_kh = khs.last().expect("have one");
match r {
Ok(path) => {
match config.output_format {
crate::output::OutputFormat::DOT => {
eprintln!(
"DOT output for \"sq wot path\" is not yet \
implemented!");
}
_ => {
print_path_header(
target_kh,
&userid,
path.amount(),
required_amount,
);
print_path(&path, &userid, " ");
}
};
if path.amount() >= required_amount {
std::process::exit(0);
}
}
Err(err) => {
match config.output_format {
crate::output::OutputFormat::DOT => {
eprintln!(
"DOT output for \"sq wot path\" is not yet \
implemented!");
}
_ => {
print_path_header(
target_kh,
&userid,
0,
required_amount,
);
print_path_error(err);
}
};
}
}
std::process::exit(1);
}
struct KeyServerUpdate {
}
impl StatusListener for KeyServerUpdate {
fn update(&self, update: &StatusUpdate) {
eprintln!("{}", update);
}
}
pub fn dispatch(config: Config, cli: wot_cli::Command) -> Result<()> {
tracer!(TRACE, "wot::dispatch");
// Build the network.
let cert_store = match config.cert_store() {
Ok(Some(cert_store)) => cert_store,
Ok(None) => {
return Err(anyhow::anyhow!("Certificate store has been disabled"));
}
Err(err) => {
return Err(err).context("Opening certificate store");
}
};
let cert_store = CertStore::from_store(
cert_store, &config.policy, config.time);
let n = wot::Network::new(cert_store)?;
let roots = wot::Roots::new(config.trust_roots());
let mut q = wot::QueryBuilder::new(&n);
q.roots(roots);
if cli.certification_network {
q.certification_network();
}
let q = q.build();
match &cli.subcommand {
wot_cli::Subcommand::Authenticate { cert, userid, .. } => {
// Authenticate a given binding.
authenticate(
&config, &cli, &q, cli.gossip, Some(userid), Some(cert))?;
}
wot_cli::Subcommand::Lookup { userid, .. } => {
// Find all authenticated bindings for a given
// User ID, list the certificates.
authenticate(
&config, &cli, &q, cli.gossip, Some(userid), None)?;
}
wot_cli::Subcommand::Identify { cert, .. } => {
// Find and list all authenticated bindings for a given
// certificate.
authenticate(
&config, &cli, &q, cli.gossip, None, Some(cert))?;
}
wot_cli::Subcommand::List { .. } => {
// List all authenticated bindings.
authenticate(
&config, &cli, &q, cli.gossip, None, None)?;
}
wot_cli::Subcommand::Path { .. } => {
check_path(
&config, &cli, &q, &config.policy)?;
}
}
Ok(())
}

View File

@ -0,0 +1,40 @@
use openpgp::packet::UserID;
use openpgp::Fingerprint;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use sequoia_wot as wot;
use wot::Path;
mod dot;
pub use dot::DotOutputNetwork;
mod human_readable;
pub use human_readable::print_path;
pub use human_readable::print_path_error;
pub use human_readable::print_path_header;
pub use human_readable::HumanReadableOutputNetwork;
/// Trait to implement adding of Paths and outputting them in a specific format
///
/// This trait is implemented to consume a vector of Path, trust amount tuples,
/// a target Fingerprint, a target UserID, and aggregated trust amount (for the
/// target UserID) to allow further processing and eventual output in a desired
/// output format.
pub trait OutputType {
/// Add Paths for a UserID associated with a Fingerprint
///
/// Paths are provided in a vector of Path, trust amount tuples.
/// The aggregated_amount represents the (total) trust amount (derived from
/// the Paths) for the UserID associated with the Fingerprint
fn add_paths(
&mut self,
paths: Vec<(Path, usize)>,
fingerprint: &Fingerprint,
userid: &UserID,
aggregated_amount: usize,
) -> Result<()>;
/// Output the data consumed via add_paths() in a specific output format
fn finalize(&mut self) -> Result<()>;
}

View File

@ -0,0 +1,817 @@
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::io::Write;
use std::time::SystemTime;
use dot_writer::Attributes;
use dot_writer::DotWriter;
use dot_writer::Scope;
use dot_writer::Shape;
use openpgp::packet::UserID;
use openpgp::Fingerprint;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use sequoia_wot as wot;
use wot::Depth;
use wot::Path;
use wot::Roots;
use wot::FULLY_TRUSTED;
use crate::commands::wot::output::OutputType;
const DOT_INSTRUCTIONS: &'static str = "\
//
// Example: To convert DOT to SVG (on many systems):
//
// sq --output-format dot wot ... | dot -Tsvg -o output.svg
//
// For further information on using graphviz see:
// https://graphviz.org/doc/info/command.html
";
const DOT_ROOT_FILL_COLOR: &'static str = "mediumpurple2";
const DOT_TARGET_OK_FILL_COLOR: &'static str = "lightgreen";
const DOT_TARGET_FAIL_FILL_COLOR: &'static str = "indianred2";
const DOT_NODE_FILL_COLOR: &'static str = "grey";
/// Return UserID as String and remove (backslash escaped) double quotes
///
/// In quoted strings in DOT, the only escaped character is double-quote (").
/// That is, in quoted strings, the dyad \" is converted to "; all other
/// characters are left unchanged. In particular, \\ remains \\. Layout engines
/// may apply additional escape sequences.
fn escape_userid(userid: &UserID) -> String {
format!("{}", userid)
.replace("\\", "\\\\")
.replace("\"", "\\\"")
}
/// Add a legend graph to an existing Scope
///
/// The legend graph provides information on color coding of the various nodes,
/// as well as the targeted trust amount, whether looking at gossip and whether
/// the data is used as a certification network.
fn add_legend_graph(
container: &mut Scope,
required_amount: usize,
gossip: bool,
certification_network: bool,
) {
let mut legend = container.cluster();
legend.set_label("Graph legend");
legend
.node_attributes()
.set("shape", "note", false)
.set_fill_color(dot_writer::Color::White);
let mut legend_edges = Vec::new();
legend_edges.push(format!("\"Trust root\""));
legend
.node_named(legend_edges.last().expect("Just added a legend node."))
.set("fillcolor", DOT_ROOT_FILL_COLOR, false);
legend_edges.push(format!("\"Intermediate introducer\""));
legend
.node_named(legend_edges.last().expect("Just added a legend node."))
.set("fillcolor", DOT_NODE_FILL_COLOR, false);
legend_edges.push(format!("\"Authenticated target\""));
legend
.node_named(legend_edges.last().expect("Just added a legend node."))
.set("fillcolor", DOT_TARGET_OK_FILL_COLOR, false);
legend_edges.push(format!("\"Unauthenticated target\""));
legend
.node_named(legend_edges.last().expect("Just added a legend node."))
.set("fillcolor", DOT_TARGET_FAIL_FILL_COLOR, false);
legend_edges.push(format!(
"\"target trust amount: {}%\"",
(100 * required_amount) / FULLY_TRUSTED,
));
legend.node_named(legend_edges.last().expect("Just added a legend node."));
if gossip {
legend_edges.push(String::from("gossip"));
legend.node_named(
legend_edges.last().expect("Just added a legend node."),
);
}
if certification_network {
legend_edges.push(String::from("certification network"));
legend.node_named(
legend_edges.last().expect("Just added a legend node."),
);
}
// internal edges are used for arranging nodes in the cluster
// and are therefore invisible
let mut edge_attributes = legend.edge_attributes();
edge_attributes
.set_font_size(0.1)
.set_style(dot_writer::Style::Invisible)
.set_arrow_size(0.1)
.set_arrow_tail(dot_writer::ArrowType::InvEmpty);
drop(edge_attributes);
// add edges for all legend nodes so that they can be arranged within the
// cluster
for edge in legend_edges.windows(2) {
legend.edge(&edge[0], &edge[1]);
}
}
/// The output representation of a certification
///
/// An OutputCertification tracks the issuer's and target's Fingerprint, as well
/// as the target's UserID.
/// Furthermore, the trust amount and depth of the certification and its
/// (optional) creation and expiry timestamps are covered.
#[derive(Clone, Debug)]
pub struct OutputCertification {
issuer_fingerprint: Fingerprint,
target_fingerprint: Fingerprint,
target_uid: UserID,
creation: SystemTime,
expiry: Option<SystemTime>,
trust_amount: usize,
depth: Depth,
}
impl OutputCertification {
pub fn new(
issuer_fingerprint: Fingerprint,
target_fingerprint: Fingerprint,
target_uid: UserID,
creation: SystemTime,
expiry: Option<SystemTime>,
trust_amount: usize,
depth: Depth,
) -> Self {
Self {
issuer_fingerprint,
target_fingerprint,
target_uid,
creation,
expiry,
trust_amount,
depth,
}
}
}
/// The output representation of a Path
///
/// A number uniquely identifies an OutputPath amongst others.
#[derive(Debug)]
pub struct OutputPath {
// The unique number of the OutputPath (an OutputNetwork provides between
// 0 and n OutputPaths per target Fingerprint)
number: usize,
certifications: Vec<OutputCertification>,
}
impl OutputPath {
pub fn new(number: usize) -> Self {
Self {
number,
certifications: Vec::new(),
}
}
/// Return the certifications of the OutputPath in an iterator
pub fn certifications(&self) -> impl Iterator<Item = &OutputCertification> {
self.certifications.iter()
}
/// Add an OutputCertification to the list of certifications
pub fn add_certification(
&mut self,
issuer_fingerprint: Fingerprint,
target_fingerprint: Fingerprint,
target_uid: UserID,
creation: SystemTime,
expiry: Option<SystemTime>,
trust_amount: usize,
depth: Depth,
) {
self.certifications.push(OutputCertification::new(
issuer_fingerprint,
target_fingerprint,
target_uid,
creation,
expiry,
trust_amount,
depth,
))
}
}
/// The output representation of a cert
///
/// It tracks the Fingerprint and UserIDs (as well as their trust amount and
/// indicator whether they are a target of a Path) and an indicator whether the
/// Fingerprint serves as trust root.
#[derive(Clone, Debug)]
pub struct OutputCert {
keyhandle: Fingerprint,
/// HashMap tracking UserID and accompanying trust amount and whether the
/// UserID is the target of a Path
userids: HashMap<UserID, (usize, bool)>,
// does the cert serve as trust root?
is_root: bool,
}
impl OutputCert {
pub fn new(
keyhandle: &Fingerprint,
userid: UserID,
trust_amount: usize,
is_root: bool,
is_target: bool,
) -> Self {
Self {
keyhandle: keyhandle.clone(),
userids: HashMap::from([(userid, (trust_amount, is_target))]),
is_root,
}
}
/// Get the data for a provided UserID
pub fn get_userid_data(&self, userid: &UserID) -> Option<&(usize, bool)> {
self.userids.get(userid)
}
/// Add a UserID and its associated data to the list of userids
pub fn add_userid_data(&mut self, userid: UserID, data: (usize, bool)) {
self.userids.insert(userid, data);
}
/// Update the trust amount of a UserID
///
/// If no matching UserID is found, it is first created
/// (the bool indicating whether the UserID is the target of a Path is set
/// to false)
pub fn update_trust_amount(
&mut self,
userid: &UserID,
trust_amount: usize,
) {
match self.userids.get_mut(userid) {
Some(userid_data) => {
userid_data.0 = trust_amount;
}
None => {
self.add_userid_data(userid.to_owned(), (trust_amount, false));
}
}
}
/// Update whether the OutputCert serves as trust root
///
/// Once this value is set to true it is not set to false anymore
pub fn set_is_root(&mut self, is_root: bool) {
self.is_root = self.is_root || is_root
}
/// Update whether a UserID is the target of a Path
///
/// Once this value is set to true it is not set to false anymore
pub fn set_is_target(&mut self, userid: &UserID, is_target: bool) {
if let Some(userid_data) = self.userids.get_mut(userid) {
userid_data.1 = userid_data.1 || is_target;
}
}
}
/// The output representation of a Network
///
/// An OutputNetwork tracks the required trust amount for the network, a list
/// of OutputCerts in the network, and a hash map containing key-value pairs
/// consisting of Path target Fingerprints and lists of OutputPaths.
#[derive(Debug)]
pub struct OutputNetwork {
required_amount: usize,
gossip: bool,
certification_network: bool,
certs: Vec<OutputCert>,
paths: HashMap<Fingerprint, Vec<OutputPath>>,
}
impl OutputNetwork {
pub fn new(
required_amount: usize,
gossip: bool,
certification_network: bool,
) -> Self {
let certs = Vec::new();
let paths = HashMap::new();
OutputNetwork {
required_amount,
gossip,
certification_network,
certs,
paths,
}
}
/// Try to add an OutputCert and return it
///
/// If no OutputCert identified by keyhandle exists, one is created.
pub fn try_add_cert(
&mut self,
keyhandle: Fingerprint,
userid: UserID,
trust_amount: usize,
is_root: bool,
is_target: bool,
) -> &OutputCert {
// take `self.certs` out of `self` to help the borrow checker
let mut certs = std::mem::replace(&mut self.certs, vec![]);
if let Some(cert) = Self::get_mut_cert(&mut certs, &keyhandle) {
if cert.get_userid_data(&userid).is_none() {
cert.add_userid_data(
userid.to_owned(),
(trust_amount, is_target),
);
}
} else {
certs.push(OutputCert::new(
&keyhandle,
userid,
trust_amount,
is_root,
is_target,
));
}
self.certs = certs;
self.get_cert(&keyhandle).expect("A cert was just added")
}
/// Return an OutputCert matching a Fingerprint
pub fn get_cert(&self, keyhandle: &Fingerprint) -> Option<&OutputCert> {
self.certs.iter().find(|x| &x.keyhandle == keyhandle)
}
/// Get a mutable reference to an OutputCert matching a Fingerprint
pub fn get_mut_cert<'a>(
certs: &'a mut Vec<OutputCert>,
keyhandle: &Fingerprint,
) -> Option<&'a mut OutputCert> {
if let Some(i) = certs.iter().position(|x| &x.keyhandle == keyhandle) {
Some(&mut certs[i])
} else {
None
}
}
/// Get a mutable reference to an OutputPath matching a Fingerprint (of an
/// OutputCert) and a number (of a specific OutputPath)
pub fn get_mut_path(
&mut self,
fingerprint: &Fingerprint,
number: usize,
) -> Option<&mut OutputPath> {
if let Some(path_list) = self.paths.get_mut(fingerprint) {
path_list.iter_mut().filter(|x| x.number == number).last()
} else {
None
}
}
/// Add an empty OutputPath to the list of paths using a Fingerprint and a
/// number
pub fn add_path(&mut self, fingerprint: &Fingerprint, number: usize) {
if let Some(path_list) = self.paths.get_mut(fingerprint) {
path_list.push(OutputPath::new(number));
} else {
let mut path_list = Vec::new();
path_list.push(OutputPath::new(number));
self.paths.insert(fingerprint.clone(), path_list);
}
}
/// Add an OutputCertification to a list of OutputPaths matching a
/// Fingerprint and a path number
pub fn add_certification(
&mut self,
path_target_fingerprint: &Fingerprint,
path_number: usize,
target_fingerprint: Fingerprint,
target_uid: UserID,
issuer_fingerprint: Fingerprint,
creation: SystemTime,
expiry: Option<SystemTime>,
trust_amount: usize,
depth: Depth,
) {
self.add_path(&path_target_fingerprint, path_number);
if let Some(path) =
self.get_mut_path(&path_target_fingerprint, path_number)
{
path.add_certification(
issuer_fingerprint,
target_fingerprint,
target_uid,
creation,
expiry,
trust_amount,
depth,
);
} else {
panic!(
"There is no path associated with keyhandle {} and number {}!",
target_fingerprint, path_number
);
}
}
/// Return the OutputCerts in an Iterator
pub fn certs(&self) -> impl Iterator<Item = &OutputCert> {
self.certs.iter()
}
/// Return an Iterator of tuples of Fingerprint and OutputPaths
pub fn paths(
&self,
) -> impl Iterator<Item = (&Fingerprint, &Vec<OutputPath>)> {
self.paths.iter()
}
/// Create the edge label for a certification
fn create_certification_edge_label(
trust_amount: usize,
creation: Option<SystemTime>,
expiry: Option<SystemTime>,
depth: Option<Depth>,
) -> String {
let mut certification_label = String::new();
if trust_amount < FULLY_TRUSTED {
certification_label.push_str(&format!(
"partially certified (amount: {} of 120)",
trust_amount,
));
} else {
certification_label.push_str("certified");
}
if let Some(time) = creation {
certification_label.push_str(&format!(
" on {}",
chrono::DateTime::<chrono::Utc>::from(time).format("%Y-%m-%d")
));
}
if let Some(time) = expiry {
certification_label.push_str(&format!(
" (expiry: {})",
chrono::DateTime::<chrono::Utc>::from(time).format("%Y-%m-%d")
));
}
if creation.is_some() || expiry.is_some() {
certification_label.push_str("\n");
}
match depth {
Some(Depth::Limit(depth)) => {
if depth > 0 {
certification_label.push_str(" as a");
if trust_amount != FULLY_TRUSTED {
certification_label.push_str(&format!(
" partially trusted ({} of 120)",
trust_amount,
));
} else {
certification_label.push_str(" fully trusted");
}
if depth == 1 {
certification_label.push_str(&format!(
" introducer (depth: {})",
depth,
));
} else {
certification_label.push_str(&format!(
" meta-introducer (depth: {})",
depth,
));
}
}
}
Some(Depth::Unconstrained) => {
certification_label.push_str(" as a");
if trust_amount != FULLY_TRUSTED {
certification_label.push_str(&format!(
" partially trusted ({} of 120)",
trust_amount,
));
} else {
certification_label.push_str(" fully trusted");
}
certification_label.push_str(" issuer (depth: infinite)");
}
_ => {}
}
certification_label
}
/// Write the OutputNetwork to an output (in DOT format)
pub fn dot(&self, writer: &mut dyn Write) -> Result<()> {
let mut output_bytes = Vec::new();
let mut dot_writer = DotWriter::from(&mut output_bytes);
dot_writer.set_pretty_print(true);
// the base graph with all relevant node settings
let mut base_graph = dot_writer.digraph();
base_graph
.node_attributes()
.set_shape(Shape::Rectangle)
.set_style(dot_writer::Style::Filled);
// container cluster for all further clusters and nodes
let mut container = base_graph.cluster();
for target_cert in self.certs() {
let mut cert_cluster = container.cluster();
cert_cluster.set("color", DOT_NODE_FILL_COLOR, false);
// internal edges are used for arranging nodes in the cluster
// and are therefore invisible
let mut edge_attributes = cert_cluster.edge_attributes();
edge_attributes.set_style(dot_writer::Style::Invisible);
drop(edge_attributes);
// sort the UserIDs and accompanying data by reverse amount and
// UserID
let mut userid_data =
target_cert.userids.iter().collect::<Vec<_>>();
userid_data.sort_by(|a, b| b.1 .0.cmp(&a.1 .0).then(a.0.cmp(&b.0)));
// add all edges between Fingerprints and the foreign (to their own
// key) UserIDs they are certifying
let mut cert_edges = Vec::new();
for (userid, (trust_amount, is_target)) in userid_data {
let node_name = format!(
"\"{}_{}\"",
&target_cert.keyhandle,
escape_userid(&userid)
);
cert_edges.push(node_name.clone());
let mut node = cert_cluster.node_named(&node_name);
// if it is a trust root or not a target of a path, we do not
// need to add trust amount
if target_cert.is_root || !is_target {
node.set_label(&format!("{}", escape_userid(&userid)));
} else {
node.set_label(&format!(
"{}\n({}%)",
escape_userid(&userid),
(trust_amount * 100) / FULLY_TRUSTED,
));
}
node.set(
"fillcolor",
if *is_target {
if trust_amount >= &self.required_amount {
DOT_TARGET_OK_FILL_COLOR
} else {
DOT_TARGET_FAIL_FILL_COLOR
}
} else {
DOT_NODE_FILL_COLOR
},
false,
);
}
// add node for Fingerprint
let node_name = format!("\"{}\"", &target_cert.keyhandle);
cert_edges.push(node_name.clone());
let mut keyhandle_node = cert_cluster.node_named(&node_name);
keyhandle_node.set_label(&format!("{}", target_cert.keyhandle));
if target_cert.is_root {
keyhandle_node.set("fillcolor", DOT_ROOT_FILL_COLOR, false);
}
drop(keyhandle_node);
// add edges for all UserID and the Fingerprint nodes so that they
// can be arranged within the cluster
for edge in cert_edges.windows(2) {
cert_cluster.edge(&edge[0], &edge[1]);
}
}
// add edges for all certifications of Fingerprints on UserIDs
let mut known_certifications = BTreeSet::new();
for (_keyhandle, paths) in self.paths() {
for path in paths.iter() {
for certification in path.certifications() {
let entry = format!(
"{}_{}_{}",
&certification.issuer_fingerprint,
&certification.target_fingerprint,
&certification.target_uid,
);
if !known_certifications.contains(&entry) {
// as gossip output is likely already very convoluted,
// do not include self-signatures
if !(self.gossip
&& &certification.issuer_fingerprint
== &certification.target_fingerprint)
|| !self.gossip
{
let edge = container.edge(
format!(
"\"{}\"",
&certification.issuer_fingerprint
),
format!(
"\"{}_{}\"",
&certification.target_fingerprint,
escape_userid(&certification.target_uid),
),
);
let certification_label =
OutputNetwork::create_certification_edge_label(
certification.trust_amount,
Some(certification.creation),
certification.expiry,
Some(certification.depth),
);
// use xlabel when generating gossip output,
// so it is less likely to run into init_rank
// issues: https://gitlab.com/graphviz/graphviz/-/issues/1213
if self.gossip {
edge.attributes()
.set("xlabel", &certification_label, true)
.set("decorate", "true", false);
} else {
edge.attributes()
.set_label(&certification_label)
.set("decorate", "true", false);
}
known_certifications.insert(entry);
}
}
}
}
}
// add a legend graph
add_legend_graph(
&mut container,
self.required_amount,
self.gossip,
self.certification_network,
);
drop(container);
drop(base_graph);
if let Ok(data) = String::from_utf8(output_bytes) {
writeln!(
writer,
"// Created by {} {}",
env!("CARGO_BIN_NAME"),
env!("CARGO_PKG_VERSION")
)?;
writeln!(writer, "{}", DOT_INSTRUCTIONS)?;
writeln!(writer, "{}", data)?;
}
Ok(())
}
}
/// The DOT specific implementation of an OutputNetwork representation
///
/// DotOutputNetwork tracks an OutputNetwork and the roots for it.
pub struct DotOutputNetwork<'a> {
output_network: OutputNetwork,
roots: &'a Roots,
}
impl<'a> DotOutputNetwork<'a> {
/// Create a new DotOutputNetwork
pub fn new(
required_amount: usize,
roots: &'a Roots,
gossip: bool,
certification_network: bool,
) -> Self {
let output_network =
OutputNetwork::new(required_amount, gossip, certification_network);
Self {
output_network,
roots,
}
}
}
impl<'a> OutputType for DotOutputNetwork<'a> {
/// Add paths to the OutputNetwork
fn add_paths(
&mut self,
paths: Vec<(Path, usize)>,
fingerprint: &Fingerprint,
userid: &UserID,
aggregated_amount: usize,
) -> Result<()> {
match OutputNetwork::get_mut_cert(
&mut self.output_network.certs,
&fingerprint.to_owned(),
) {
Some(cert) => {
cert.update_trust_amount(userid, aggregated_amount);
cert.set_is_root(self.roots.is_root(fingerprint));
cert.set_is_target(userid, true);
}
None => {
self.output_network.try_add_cert(
fingerprint.to_owned(),
userid.to_owned(),
aggregated_amount,
self.roots.is_root(fingerprint),
true,
);
}
}
for (path_number, (path, _path_trust_amount)) in
paths.iter().enumerate()
{
let issuer_fingerprint = path.root().fingerprint();
if self.output_network.get_cert(&issuer_fingerprint).is_none() {
let certifier_userid = if path.certifications().count() == 0 {
userid
} else if let Some(userid) = path.root().primary_userid() {
userid.userid()
} else {
userid
};
self.output_network.try_add_cert(
issuer_fingerprint,
certifier_userid.to_owned(),
0,
self.roots.is_root(&path.root().fingerprint()),
false,
);
}
// sort the certifications by reverse amount and issuer
let mut certifications = path.certifications().collect::<Vec<_>>();
certifications.sort_by(|a, b| {
b.amount().cmp(&a.amount()).then(
a.issuer().fingerprint().cmp(&b.issuer().fingerprint()),
)
});
for certification in certifications {
let certification_target_userid =
if let Some(target_userid) = certification.userid() {
target_userid
} else {
userid
};
let target_cert_is_root =
self.roots.is_root(certification.target().fingerprint());
self.output_network.try_add_cert(
certification.target().fingerprint(),
certification_target_userid.to_owned(),
0,
target_cert_is_root,
false,
);
self.output_network.add_certification(
&path.target().fingerprint(),
path_number,
certification.target().fingerprint(),
certification_target_userid.to_owned(),
certification.issuer().fingerprint(),
certification.creation_time(),
certification.expiration_time(),
certification.amount(),
certification.depth(),
)
}
}
Ok(())
}
/// Write the DotOutputNetwork to output
fn finalize(&mut self) -> Result<()> {
self.output_network.dot(&mut std::io::stdout())?;
Ok(())
}
}

View File

@ -0,0 +1,261 @@
use anyhow::Error;
use openpgp::packet::UserID;
use openpgp::Fingerprint;
use openpgp::KeyHandle;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use sequoia_wot as wot;
use wot::Path;
use wot::PathLints;
use wot::FULLY_TRUSTED;
use wot::PARTIALLY_TRUSTED;
use crate::error_chain;
use crate::commands::wot::output::OutputType;
/// Prints a Path Error
pub fn print_path_error(err: Error) {
println!("└ Checking path: {}", err);
}
/// Prints information of a Path for a target UserID associated with a KeyHandle
pub fn print_path_header(
target_kh: &KeyHandle,
target_userid: &UserID,
amount: usize,
required_amount: usize,
) {
println!(
"[{}] {} {}: {} authenticated ({}%)",
if amount >= required_amount {
""
} else {
" "
},
target_kh,
String::from_utf8_lossy(target_userid.value()),
if amount >= 2 * FULLY_TRUSTED {
"doubly"
} else if amount >= FULLY_TRUSTED {
"fully"
} else if amount >= PARTIALLY_TRUSTED {
"partially"
} else if amount > 0 {
"marginally"
} else {
"not"
},
(amount * 100) / FULLY_TRUSTED
);
}
/// Prints information on a Path for a UserID
pub fn print_path(path: &PathLints, target_userid: &UserID, prefix: &str) {
let certification_count = path.certifications().count();
print!("{}{}", prefix, path.root().key_handle());
if certification_count == 0 {
print!(" {:?}", String::from_utf8_lossy(target_userid.value()));
} else if let Some(userid) = path.root().primary_userid() {
print!(" ({:?})", String::from_utf8_lossy(userid.value()));
}
println!("");
for (last, (cert, certification)) in path
.certs()
.zip(path.certifications())
.enumerate()
.map(|(j, c)| {
if j + 1 == certification_count {
(true, c)
} else {
(false, c)
}
})
{
print!("{}", prefix);
if let Some(certification) = certification.certification() {
if certification.amount() < FULLY_TRUSTED {
print!(
" partially certified (amount: {} of 120)",
certification.amount()
);
} else {
print!(" certified");
}
if last {
print!(" the following binding");
} else {
print!(" the following certificate");
}
print!(
" on {}",
chrono::DateTime::<chrono::Utc>::from(
certification.creation_time()
)
.format("%Y-%m-%d")
);
if let Some(e) = certification.expiration_time() {
print!(
" (expiry: {})",
chrono::DateTime::<chrono::Utc>::from(e).format("%Y-%m-%d")
);
}
if certification.depth() > 0.into() {
print!(" as a");
if certification.amount() != FULLY_TRUSTED {
print!(
" partially trusted ({} of 120)",
certification.amount()
);
} else {
print!(" fully trusted");
}
if certification.depth() == 1.into() {
print!(" introducer (depth: {})", certification.depth());
} else {
print!(
" meta-introducer (depth: {})",
certification.depth()
);
}
}
} else {
print!(" No adequate certification found.");
}
println!("");
for err in cert.errors().iter().chain(cert.lints()) {
for (i, msg) in error_chain(err).into_iter().enumerate() {
println!(
"{}│ {}{}",
prefix,
if i == 0 { "" } else { " " },
msg
);
}
}
for err in certification.errors().iter().chain(certification.lints()) {
for (i, msg) in error_chain(err).into_iter().enumerate() {
println!(
"{}│ {}{}",
prefix,
if i == 0 { "" } else { " " },
msg
);
}
}
print!(
"{}{} {}",
prefix,
if last { "" } else { "" },
certification.target()
);
if last {
print!(" {:?}", String::from_utf8_lossy(target_userid.value()));
} else {
if let Some(userid) =
certification.target_cert().and_then(|c| c.primary_userid())
{
print!(" ({:?})", String::from_utf8_lossy(userid.value()));
}
}
println!("");
if last {
let target = path.certs().last().expect("have one");
for err in target.errors().iter().chain(target.lints()) {
for (i, msg) in error_chain(err).into_iter().enumerate() {
println!(
"{} {}{}",
prefix,
if i == 0 { "" } else { " " },
msg
);
}
}
}
}
println!("");
}
/// The human-readable specific implementation of an OutputNetwork
///
/// HumanReadableOutputNetwork tracks the target trust amount for the network
/// and whether it displays "gossip".
pub struct HumanReadableOutputNetwork {
gossip: bool,
required_amount: usize,
}
impl HumanReadableOutputNetwork {
/// Create a new HumanReadableOutputNetwork
pub fn new(required_amount: usize, gossip: bool) -> Self {
Self {
required_amount,
gossip,
}
}
}
impl OutputType for HumanReadableOutputNetwork {
/// Add paths to the OutputNetwork and display them directly
fn add_paths(
&mut self,
paths: Vec<(Path, usize)>,
fingerprint: &Fingerprint,
userid: &UserID,
aggregated_amount: usize,
) -> Result<()> {
let kh = KeyHandle::from(fingerprint);
if !self.gossip {
print_path_header(
&kh,
userid,
aggregated_amount,
self.required_amount,
);
}
for (i, (path, amount)) in paths.iter().enumerate() {
let prefix = if self.gossip {
print_path_header(
&kh,
userid,
aggregated_amount,
self.required_amount,
);
" "
} else {
if !self.gossip && paths.len() > 1 {
println!(
" Path #{} of {}, trust amount {}:",
i + 1,
paths.len(),
amount
);
" "
} else {
" "
}
};
print_path(&path.into(), userid, prefix)
}
Ok(())
}
/// Write the HumanReadableOutputNetwork to output
///
/// This function does in fact nothing as we are printing directly in
/// add_paths().
fn finalize(&mut self) -> Result<()> {
Ok(())
}
}

92
src/log.rs Normal file
View File

@ -0,0 +1,92 @@
#![allow(unused_macros)]
use std::cell::RefCell;
// Like eprintln!
macro_rules! log {
($dst:expr $(,)?) => (
eprintln!("{}", $dst)
);
($dst:expr, $($arg:tt)*) => (
eprintln!("{}", std::format!($dst, $($arg)*))
);
}
// The indent level. It is increased with each call to tracer and
// decremented when the tracer goes out of scope.
thread_local! {
pub static INDENT_LEVEL: RefCell<usize> = RefCell::new(0);
}
// Like eprintln!, but the first argument is a boolean, which
// indicates if the string should actually be printed.
macro_rules! trace {
( $TRACE:expr, $fmt:expr, $($pargs:expr),* ) => {
if $TRACE {
let indent_level = crate::log::INDENT_LEVEL.with(|i| {
*i.borrow()
});
let ws = " ";
log!("{}{}",
&ws[0..std::cmp::min(ws.len(), std::cmp::max(1, indent_level) - 1)],
format!($fmt, $($pargs),*));
}
};
( $TRACE:expr, $fmt:expr ) => {
trace!($TRACE, $fmt, );
};
}
macro_rules! tracer {
( $TRACE:expr, $func:expr ) => {
// Currently, Rust doesn't support $( ... ) in a nested
// macro's definition. See:
// https://users.rust-lang.org/t/nested-macros-issue/8348/2
#[allow(unused)]
macro_rules! t {
( $fmt:expr ) =>
{ trace!($TRACE, "{}: {}", $func, $fmt) };
( $fmt:expr, $a:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a)) };
( $fmt:expr, $a:expr, $b:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j)) };
( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr ) =>
{ trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)) };
}
struct Indent {}
impl Indent {
fn init() -> Self {
crate::log::INDENT_LEVEL.with(|i| {
i.replace_with(|i| *i + 1);
});
Indent {}
}
}
impl Drop for Indent {
fn drop(&mut self) {
crate::log::INDENT_LEVEL.with(|i| {
i.replace_with(|i| *i - 1);
});
}
}
let _indent = Indent::init();
}
}

View File

@ -27,6 +27,13 @@ pub enum OutputFormat {
/// Output as JSON.
Json,
/// Output as DOT.
///
/// This format is supported by a few commands that emit a
/// graphical network. In particular, the \"sq wot\" subcommands
/// can emit this format.
DOT,
}
impl FromStr for OutputFormat {
@ -36,6 +43,7 @@ impl FromStr for OutputFormat {
match s {
"human-readable" => Ok(Self::HumanReadable),
"json" => Ok(Self::Json),
"dot" => Ok(Self::DOT),
_ => Err(anyhow!("unknown output format {:?}", s)),
}
}
@ -181,14 +189,14 @@ impl Model {
match self {
Self::KeyringListV0(x) => {
match format {
OutputFormat::HumanReadable => x.human_readable(w)?,
OutputFormat::Json => x.json(w)?
OutputFormat::Json => x.json(w)?,
_ => x.human_readable(w)?,
}
}
Self::WkdUrlV0(x) => {
match format {
OutputFormat::HumanReadable => x.human_readable(w)?,
OutputFormat::Json => x.json(w)?
OutputFormat::Json => x.json(w)?,
_ => x.human_readable(w)?,
}
}
}

View File

@ -52,12 +52,15 @@ use sequoia_wot as wot;
use wot::store::Store as _;
use clap::FromArgMatches;
use crate::sq_cli::packet;
use sq_cli::SqSubcommands;
#[macro_use] mod macros;
#[macro_use] mod log;
mod sq_cli;
use sq_cli::packet;
use sq_cli::SqSubcommands;
mod man;
mod commands;
pub mod output;
@ -345,8 +348,8 @@ pub struct Config<'a> {
no_rw_cert_store: bool,
cert_store_path: Option<PathBuf>,
keyrings: Vec<PathBuf>,
// This will be set if the cert store is enabled (--no-cert-store
// is not passed), OR --keyring is passed.
// This will be set if --no-cert-store is not passed, OR --keyring
// is passed.
cert_store: OnceCell<cert_store::CertStore<'a>>,
// The value of --trust-root.
@ -752,12 +755,9 @@ impl<'store> Config<'store> {
};
if matches.is_empty() {
if error.is_none() {
error = Some(anyhow::anyhow!(
"No certificates are associated with {:?}",
userid));
}
continue;
return Err(anyhow::anyhow!(
"No certificates are associated with {:?}",
userid));
}
struct Entry {
@ -1450,6 +1450,10 @@ fn main() -> Result<()> {
SqSubcommands::Link(command) => {
commands::link::link(config, command)?
}
SqSubcommands::Wot(command) => {
commands::wot::dispatch(config, command)?
}
}
Ok(())

View File

@ -28,6 +28,7 @@ pub mod revoke;
mod sign;
mod verify;
pub mod wkd;
pub mod wot;
pub mod types;
@ -110,7 +111,7 @@ store, and the results are merged together."
#[clap(
long = "output-format",
value_name = "FORMAT",
value_parser = ["human-readable", "json"],
value_parser = ["human-readable", "json", "dot"],
default_value = "human-readable",
env = "SQ_OUTPUT_FORMAT",
help = "Produces output in FORMAT, if possible",
@ -202,6 +203,7 @@ pub enum SqSubcommands {
Export(export::Command),
Certify(certify::Command),
Link(link::Command),
Wot(wot::Command),
#[cfg(feature = "autocrypt")]
Autocrypt(autocrypt::Command),

495
src/sq_cli/wot.rs Normal file
View File

@ -0,0 +1,495 @@
/// Command-line parser for sq wot.
use std::ops::Deref;
use clap::Parser;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use openpgp::Result;
use openpgp::packet::UserID;
/// A frontend for Sequoia's web-of-trust engine.
///
/// This subcommand presents a CLI to query a web-of-trust network.
///
/// Functionality is grouped and available using subcommands.
///
/// We use the term `certificate`, or cert for short, to refer to
/// OpenPGP keys that do not contain secret key material. We reserve
/// the term `key` for OpenPGP certificates that also contain secret
/// key material.
#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
pub struct Command {
/// Treats all certificates as unreliable trust roots.
///
/// This option is useful for figuring out what others think about
/// a certificate (i.e., gossip or hearsay). In other words, this
/// finds arbitrary paths to a particular certificate.
///
/// Gossip is useful in helping to identify alternative ways to
/// authenticate a certificate. For instance, imagine Ed wants to
/// authenticate Laura's certificate, but asking her directly is
/// inconvenient. Ed discovers that Micah has certified Laura's
/// certificate, but Ed hasn't yet authenticated Micah's
/// certificate. If Ed is willing to rely on Micah as a trusted
/// introducer, and authenticating Micah's certificate is easier
/// than authenticating Laura's certificate, then Ed has learned
/// about an easier way to authenticate Laura's certificate.
///
/// EXAMPLES:
///
/// # Get gossip about a certificate.{n}
/// $ sq wot --keyring keyring.pgp \\{n}
/// --gossip identify 3217C509292FC67076ECD75C7614269BDDF73B36
#[arg(global=true, long)]
pub gossip: bool,
/// Treats the network as a certification network.
///
/// Normally, `sq wot` treats the web-of-trust network as an
/// authentication network where a certification only means that
/// the binding is correct, not that the target should be treated
/// as a trusted introducer. In a certification network, the
/// targets of certifications are treated as trusted introducers
/// with infinite depth, and any regular expressions are ignored.
/// Note: The trust amount remains unchanged. This is how most
/// so-called pgp path-finding algorithms work.
#[arg(global=true, long)]
pub certification_network: bool,
/// The required amount of trust.
///
/// 120 indicates full authentication; values less than 120
/// indicate partial authentication. When
/// `--certification-network` is passed, this defaults to 1200,
/// i.e., sq wot tries to find 10 paths.
#[arg(global=true, short='a', long,
conflicts_with_all=["partial", "full", "double"])]
pub trust_amount: Option<usize>,
/// Require partial authentication.
///
/// This is the same as passing `--trust-amount 40`.
#[arg(global=true, long,
conflicts_with_all=["trust_amount", "full", "double"])]
pub partial: bool,
/// Require full authentication.
///
/// This is the same as passing `--trust-amount 120`.
#[arg(global=true, long,
conflicts_with_all=["trust_amount", "partial", "double"])]
pub full: bool,
/// Require double authentication.
///
/// This is the same as passing `--trust-amount 240`.
#[arg(global=true, long,
conflicts_with_all=["trust_amount", "partial", "full"])]
pub double: bool,
#[command(subcommand)]
pub subcommand: Subcommand,
}
#[derive(clap::Subcommand, Debug)]
pub enum Subcommand {
/// Authenticate a binding.
///
/// Authenticate a binding (a certificate and User ID) by looking
/// for a path from the trust roots to the specified binding in
/// the web of trust. Because certifications may express
/// uncertainty (i.e., certifications may be marked as conveying
/// only partial or marginal trust), multiple paths may be needed.
///
/// If a binding could be authenticated to the specified level (by
/// default: fully authenticated, i.e., a trust amount of 120),
/// then the exit status is 0. Otherwise the exit status is 1.
///
/// If any valid paths to the binding are found, they are printed
/// on stdout whether they are sufficient to authenticate the
/// binding or not.
#[command(after_help("\
EXAMPLES:
# Authenticate a binding.
$ sq --keyring keyring.pgp \\
wot \\
--partial \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
authenticate \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
'Alice <alice@example.org>'
# The same as above, but this time generate output in DOT format
# and convert it to an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot authenticate \\
--partial \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
'Alice <alice@example.org>' \\
| dot -Tsvg -o alice.pgp
# Try and authenticate each binding where the User ID has the
# specified email address.
$ sq --keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot authenticate \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
--email 'alice@example.org'
# The same as above, but this time generate output in DOT format
# and convert it to an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot authenticate \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
--email 'alice@example.org' \\
| dot -Tsvg -o alice.svg
"))]
Authenticate {
#[command(flatten)]
email: EmailArg,
#[command(flatten)]
cert: CertArg,
#[command(flatten)]
userid: UserIDArg,
},
/// Lookup the certificates associated with a User ID.
///
/// Identifies authenticated bindings (User ID and certificate
/// pairs) where the User ID matches the specified User ID.
///
/// If a binding could be authenticated to the specified level (by
/// default: fully authenticated, i.e., a trust amount of 120),
/// then the exit status is 0. Otherwise the exit status is 1.
///
/// If a binding could be patially authenticated (i.e., its trust
/// amount is greater than 0), then the binding is displayed, even
/// if the trust is below the specified threshold.
#[command(after_help("\
EXAMPLES:
# Lookup a certificate with the given User ID.
$ sq --keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot lookup \\
--partial \\
'Alice <alice@example.org>'
# The same as above, but output in DOT format and convert it to
# an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot lookup \\
--partial \\
'Alice <alice@example.org>' \\
| dot -Tsvg -o alice.svg
# Lookup a certificate with the given email address.
$ sq --keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot lookup \\
--email 'alice@example.org'
# The same as above, but output in DOT format and convert it to
# an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot lookup \\
--email 'alice@example.org' \\
| dot -Tsvg -o alice.svg
"))]
Lookup {
#[command(flatten)]
email: EmailArg,
#[command(flatten)]
userid: UserIDArg,
},
/// Identify a certificate.
///
/// Identify a certificate by finding authenticated bindings (User
/// ID and certificate pairs).
///
/// If a binding could be authenticated to the specified level (by
/// default: fully authenticated, i.e., a trust amount of 120),
/// then the exit status is 0. Otherwise the exit status is 1.
///
/// If a binding could be patially authenticated (i.e., its trust
/// amount is greater than 0), then the binding is displayed, even
/// if the trust is below the specified threshold.
#[command(after_help("\
EXAMPLES:
# Identify a certificate.
$ sq --keyring keyring.pgp \\
--partial \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot identify \\
C7B1406CD2F612E9CE2136156F2DA183236153AE
# The same as above, but output in DOT format and convert it to
# an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--partial \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot identify \\
C7B1406CD2F612E9CE2136156F2DA183236153AE \\
| dot -Tsvg -o C7B1406CD2F612E9CE2136156F2DA183236153AE.svg
"))]
Identify {
#[command(flatten)]
cert: CertArg,
},
/// List all authenticated bindings (User ID and certificate
/// pairs).
///
/// Only bindings that meet the specified trust amount (by default
/// bindings that are fully authenticated, i.e., have a trust
/// amount of 120), are shown.
///
/// Even if no bindings are shown, the exit status is 0.
///
/// If --email is provided, then a pattern matches if it is a case
/// insensitive substring of the email address as-is or the
/// normalized email address. Note: unlike the email address, the
/// pattern is not normalized. In particular, puny code
/// normalization is not done on the pattern.
#[command(after_help("\
EXAMPLES:
# List all bindings for example.org that are at least partially
# authenticated.
$ sq --keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot list \\
--partial \\
@example.org
# The same as above, but output in DOT format and convert it to
# an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
--trust-root 8F17777118A33DDA9BA48E62AACB3243630052D9 \\
wot list \\
--partial \\
@example.org \\
| dot -Tsvg -o example_org.svg
"))]
List {
#[command(flatten)]
email: EmailArg,
/// A pattern to select the bindings to authenticate.
///
/// The pattern is treated as a UTF-8 encoded string and a
/// case insensitive substring search (using the current
/// locale) is performed against each User ID. If a User ID
/// is not valid UTF-8, the binding is ignored.
pattern: Option<String>,
},
/// Verify the specified path.
///
/// A path is a sequence of certificates starting at the root, and
/// a User ID. This function checks that each path segment has a
/// valid certification, which also satisfies any constraints
/// (trust amount, trust depth, regular expressions).
///
/// If a valid path is not found, then this subcommand also lints
/// the path. In particular, it report if any certifications are
/// insufficient, e.g., not enough trust depth, or invalid, e.g.,
/// because they use SHA-1, but the use of SHA-1 has been
/// disabled.
#[command(after_help("\
EXAMPLES:
# Verify that Neal ceritified Justus's certificate for a particular User ID.
$ sq --keyring keyring.pgp \\
wot path \\
8F17777118A33DDA9BA48E62AACB3243630052D9 \\
CBCD8F030588653EEDD7E2659B7DD433F254904A \\
\"Justus Winter <justus@sequoia-pgp.org>\"
# The same as above, but output in DOT format and convert it to
# an SVG using Graphviz's DOT compiler.
$ sq --format dot \\
--keyring keyring.pgp \\
wot path \\
8F17777118A33DDA9BA48E62AACB3243630052D9 \\
CBCD8F030588653EEDD7E2659B7DD433F254904A \\
\"Justus Winter <justus@sequoia-pgp.org>\" \\
| dot -Tsvg -o neal--justus.svg
"))]
Path {
#[command(flatten)]
email: EmailArg,
// This should actually be a repeatable positional argument
// (Vec<Cert>) followed by a manadatory positional argument (a
// User ID), but that is not allowed by Clap v3 and Clap v4
// even though it worked fine in Clap v2. (Curiously, it
// works in `--release` mode fine and the only error appears
// to be one caught by a `debug_assert`).
//
// https://github.com/clap-rs/clap/issues/3281
#[command(flatten)]
path: PathArg,
},
}
impl Subcommand {
pub fn email(&self) -> bool {
use Subcommand::*;
match self {
Authenticate { email, .. } => email.email,
Lookup { email, .. } => email.email,
Identify { .. } => false,
Path { email, .. } => email.email,
List { email, .. } => email.email,
}
}
}
#[derive(clap::Args, Debug)]
pub struct CertArg {
/// The fingerprint or Key ID of the certificate to authenticate.
#[arg(value_name="FINGERPRINT|KEYID")]
cert: KeyHandle
}
impl Deref for CertArg {
type Target = KeyHandle;
fn deref(&self) -> &Self::Target {
&self.cert
}
}
#[derive(clap::Args, Debug)]
pub struct PathArg {
/// A path consists of one or more certificates (designated by
/// their fingerprint or Key ID) and ending in the User ID that is
/// being authenticated.
#[arg(value_names=["FINGERPRINT|KEYID", "USERID"])]
elements: Vec<String>,
}
const PATH_DESC: &str = "\
A path consists of one or more certificates (identified by their
respective fingerprint or Key ID) and a User ID.";
impl PathArg {
fn check(&self) -> Result<()> {
if self.elements.len() < 2 {
Err(anyhow::anyhow!(
"\
The following required arguments were not provided:
{}<USERID>
{}
Usage: sq wot path <FINGERPRINT|KEYID>... <USERID>
For more information try '--help'",
if self.elements.len() == 0 {
"<FINGERPRINT|KEYID>\n "
} else {
""
},
PATH_DESC))
} else {
Ok(())
}
}
pub fn certs(&self) -> Result<Vec<KeyHandle>> {
self.check()?;
// Skip the last one. That's the User ID.
self.elements[0..self.elements.len() - 1]
.iter()
.map(|e| {
e.parse()
.map_err(|err| {
anyhow::anyhow!(
"Invalid value {:?} for '<FINGERPRINT|KEYID>': {}
{}
For more information try '--help'",
e, err, PATH_DESC)
})
})
.collect::<Result<Vec<KeyHandle>>>()
}
pub fn userid(&self) -> Result<UserID> {
self.check()?;
let userid = self.elements.last().expect("just checked");
Ok(UserID::from(userid.as_bytes()))
}
}
#[derive(clap::Args, Debug)]
pub struct UserIDArg {
/// The User ID to authenticate.
///
/// This is case sensitive, and must be the whole User ID, not
/// just a substring or an email address.
pub userid: UserID,
}
impl Deref for UserIDArg {
type Target = UserID;
fn deref(&self) -> &Self::Target {
&self.userid
}
}
#[derive(clap::Args, Debug)]
pub struct EmailArg {
/// Changes the USERID parameter to match User IDs with the
/// specified email address.
///
/// Interprets the USERID parameter as an email address, which
/// is then used to select User IDs with that email address.
///
/// Unlike when comparing User IDs, email addresses are first
/// normalized by the domain to ASCII using IDNA2008 Punycode
/// conversion, and then converting the resulting email
/// address to lowercase using the empty locale.
///
/// If multiple User IDs match, they are each considered in
/// turn, and this function returns success if at least one of
/// those User IDs can be authenticated. Note: The paths to
/// the different User IDs are not combined.
#[arg(long)]
pub email: bool,
}
impl Deref for EmailArg {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.email
}
}

View File

@ -0,0 +1,5 @@
These files are all from sequoia-wot. That repository includes
descriptions of each of these files. Do not modify these files
without propagating changes back to sequoia-wot.
https://gitlab.com/sequoia-pgp/sequoia-wot/-/tree/main/tests/data

View File

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
Tz+QZA4=
=Sesf
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,111 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEXgvhABYJKwYBBAHaRw8BAQdAmBAnRPbQ8kxpYFYNyi/iJs2L/1EvB44dVd0Z
KfrP+KXCwAsEHxYKAH0Fgl4L4QADCwkHCRCnUaegUyhjukcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeuunycYEoBL2z0M+LUMX95atzZfka5
VurCjWhshCJV6QMVCggCmwECHgEWIQQhmqtmHIqvRSbbwxqnUaegUyhjugAA4AAA
/3liOXNzpzUNGAbpFHEvKXpZD5i375uo0+9UF4IT8lpQAP4pXPPq2DSVrGDvdZrc
Dzo9aXwNI/+wz5UKPUXn2tZKC80TPGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoA
gAWCXgvhAAMLCQcJEKdRp6BTKGO6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ/6ctGt6wzg5TDkhDYXrkKhHFklkNzFEfy75uOk46fg3AxUK
CAKZAQKbAQIeARYhBCGaq2Yciq9FJtvDGqdRp6BTKGO6AABcvQEA4wZMmOev/Row
E+yFBQNo6pKYKMImemYEGPG++9ttmZ4A/3yshwGMRxbL+GWshferOFKuqXxHkbB2
SLQc5z5KFIgIzjMEXgvhABYJKwYBBAHaRw8BAQdAqjemXq25t94cpIpulUqKR8DM
9QIEyd0JLT1b1WtOub3CwL8EGBYKATEFgl4L4QAJEKdRp6BTKGO6RxQAAAAAAB4A
IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyMaTp6IIJIcan4K0PYZdR80
sNuq35+mk10oyWIuiJVKApsCvqAEGRYKAG8Fgl4L4QAJEBUzrVPCfimeRxQAAAAA
AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyYMx6BTk0pNNBiCQK4S
bOsn7Sr+2DGG4WsPrTavJtYRFiEENfd7dUHjFnnJz9mXFTOtU8J+KZ4AAP4eAQDf
sigQj6pDNm3DqoYUUTigYrFF+a6+uJaXwAXrTFh/gQD+NVZfKKbWgdtyltwxKEMY
nhkOuXee/9uIwZc1UEefHAAWIQQhmqtmHIqvRSbbwxqnUaegUyhjugAA+28A/jiq
tP+sy5i463z43clux7HrXc8QR8d3qu16VojI5G99AQDHqaEMDaiHQ0wA8BLIubIw
Kro9YvAlXr8rYbxwgQS9BcYzBF4L4QAWCSsGAQQB2kcPAQEHQKYRIkpV0aBAhIkl
ETzDmxgXlo/hzcCLhr1JsFdLDkb8wsALBB8WCgB9BYJeC+EAAwsJBwkQg/nBpHWa
FtZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JngbOv6Fgd
ebucVO+uiuSIE/dTPnOMh2MkjTn5dfyEaRwDFQoIApsBAh4BFiEERpRSkvj2Q/BX
OvcRg/nBpHWaFtYAAA4VAQC/OKE9QSY/1UecvMqndgMlJs9KKfYa0DHv21LBw17A
GQEA5tSjkgPyF2h/MsPVwcErodK4DPIUeTk1Nnw06DQEmgvNEjxkYXZlQGV4YW1w
bGUub3JnPsLADgQTFgoAgAWCXgvhAAMLCQcJEIP5waR1mhbWRxQAAAAAAB4AIHNh
bHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZycXivKq8tnrdR9nFB75kN9eQQhW
hTP2wYdP/AK0ZYRQAxUKCAKZAQKbAQIeARYhBEaUUpL49kPwVzr3EYP5waR1mhbW
AAAOowEAoQaKLFOGCqer9TZfyDpo9zBVcMsdM6dE/r5IvacVUCABAOvCcL/L+6+i
TgHmiuvB3/Mdhy0ni8Q7w/RSnUbrYiYCwsAHBBAWCgB5BYJeg9mABYMJZ5o7A4UB
eAkQdUMVfvOxK+lHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au
b3JnSSL/+MxfvpyKJNESkvF7tVtoIsYgZeZ9mTYsK6q8rVQWIQSQ4Cv7A/qgRxTR
09h1QxV+87Er6QAA4wUBAJOMv8CUv2ZQlE2scwymYNHWxkdt2OJkBzGKqSslfZ0q
AQDnb1O6/giZaDfHSKkeDttOCG/T5BBUHsaqPHdR1S/NAcLABwQQFgoAeQWCXjS/
gAWDCWeaOwOFAXgJEFafX2v7lcVERxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ1TMrkfbYGziuSEIfafiQu+U4iho+Zy+oYG+ioghtxscFiEE
v2gHEBKOa8yyJoFUVp9fa/uVxUQAALx9AP93Yz3N4TumIQRMIAMrxHmaPY7uNtF0
0tsANPZENumwhwD/XL3sKJ+lnKnk6WORg9SwFdZi6aWh4DvotUkTvzSIuwXCwAcE
EBYKAHkFgl40v4AFgwlnmjsDhQE8CRB1QxV+87Er6UcUAAAAAAAeACBzYWx0QG5v
dGF0aW9ucy5zZXF1b2lhLXBncC5vcmfoydbacr2EVGfu8SIL80LcGkwn1Caouk82
iBbDctOnkhYhBJDgK/sD+qBHFNHT2HVDFX7zsSvpAAAczwEAxshMuiJYDeD5YLZK
XRlDXe94qyhP5KkpQQvKotPHflsBALG3EybkpQInrgZoY7W4QLyvt6TI4J38e4L5
5ZwK2X4DzjMEXgvhABYJKwYBBAHaRw8BAQdA385Q2fcP6nlpCnJ3aBJorQNWcx++
UMFG2y1deO1q23LCwL8EGBYKATEFgl4L4QAJEIP5waR1mhbWRxQAAAAAAB4AIHNh
bHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ++KNdQFf6Wb5Y49++LPZYlWPBqQ
cRm3NXjJW0jCoSh6ApsCvqAEGRYKAG8Fgl4L4QAJEGydV1KFUSAcRxQAAAAAAB4A
IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZzYJ9kLnCc/hSfP/FXANg9aI
E0xMZ0lZX1CappeH8Q1NFiEEMpoUVebPBAN7a1dbbJ1XUoVRIBwAAEqMAQC6/1sN
Vll/+0wAtC6oCtGFYCvfMLScqC/HmDNbbNscJwD6AiKj9U7YierLymk3eUQNy78Y
kPvXKH/EZRW3GG6BHgoWIQRGlFKS+PZD8Fc69xGD+cGkdZoW1gAA3P4BAM76ZTrc
DX8XDtyYCuAuyj9xI6jezpGhfxKaDEEkHxJRAP0WZsHy+cDA8UVKesnQqmbec8Mv
/XitAw7LZKWg6/HMAMYzBF4L4QAWCSsGAQQB2kcPAQEHQG1SxvTSaTTNTSUO1Fv3
Dk+oVqApcqS6yMIWgWZlvhYfwsAMBCAWCgB+BYJeWvsACRB1QxV+87Er6UcUAAAA
AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcvs8dY5faQd/ZORLno
3Kl7+xbntsMskOH7FjLdTHBZ/A4dAnNvbWUgbWVzc2FnZRYhBJDgK/sD+qBHFNHT
2HVDFX7zsSvpAABrOwD/R2RnuabbidiNr2udGfJUqlmDj9EMN5DbG2ofa94DuVEB
ANHPESYhtH/9WYuHZj7CcfAnl4BEfcBBTjj4rwIcjOQMwsALBB8WCgB9BYJeC+EA
AwsJBwkQdUMVfvOxK+lHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w
Z3Aub3JnMIBwRvqbORueUzCqQ6wTO3J6OM1Hw+kJgrHjKRdkeUcDFQoIApsBAh4B
FiEEkOAr+wP6oEcU0dPYdUMVfvOxK+kAAJ0NAP0fpH/6ofJPHGY25LCNQ5ZrPxTA
Zo27QSeFzrxW0j+OxwD8CKpHFEbFYB8fBtHAjp2vIxT7RmOgbBQk1f5EGhapBAvN
ETxib2JAZXhhbXBsZS5vcmc+wsAOBBMWCgCABYJeC+EAAwsJBwkQdUMVfvOxK+lH
FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn5DxIDi+CjKIO
l3tHtGdSSVe8b7apUMFKCUOY/klX8iQDFQoIApkBApsBAh4BFiEEkOAr+wP6oEcU
0dPYdUMVfvOxK+kAAI0JAP4mnv7AMpN8fCkUhIdUq8jH6f3FxJJtcU2/fizsBZTo
sQEArrHk48dLpatStgg+1QW81Tue1/xaEgDZSWfkCylH1Q3CwAcEEBYKAHkFgl6D
2YAFgwlnmjsDhQF4CRCnUaegUyhjukcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
ZXF1b2lhLXBncC5vcmfs0nsAp2993D94jlJw5lmYiY4CVZqcdgg+dPK/JY/Z3BYh
BCGaq2Yciq9FJtvDGqdRp6BTKGO6AAA4iAD/b8/VfIxPSe2N/W2ooSoYyP1s52ll
OUqOI91nM9ngPfEBAMbQVnXxxsazyW15uZRhZ+FT6464reol//7FRi3tPZsJwsAH
BBAWCgB5BYJeNL+ABYMJZ5o7A4UCWgkQp1GnoFMoY7pHFAAAAAAAHgAgc2FsdEBu
b3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnJ/9igdtS7qdeViYr6WmMVI58UfcPfnEE
I67dsuzLUhIWIQQhmqtmHIqvRSbbwxqnUaegUyhjugAATkMA/Ahgc9Qyw5mNbaMl
dUjD8KIOVrNEPHRuq2h5zZcyAFchAQCKB1me0kiA9tfjWBcB57K5YU7xJgHJfB+0
o3DuiZCiDM4zBF4L4QAWCSsGAQQB2kcPAQEHQMAj63wXdrzDcND2k5/qn718e6vO
8FR6fspo5Wi7M0aEwsC+BBgWCgExBYJeC+EACRB1QxV+87Er6UcUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmee1UdO+WTdqpRdvI+ar5cQM8u2
nSi4kT6hOF8x49K/6AKbAr6gBBkWCgBvBYJeC+EACRBvwBwfSkPW7UcUAAAAAAAe
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfFh/jfwcIqGVuHO0Ur1j9n
yr09EH0Jv3hOZz8DxUUIqxYhBDwoJGSXGBM9vF++NW/AHB9KQ9btAAAb5wEAsHWJ
Gq5ZvXHYl+fG90BXpKxD/e3WU0Mom2q2DLydBoMA/jNUk78aQSXIoxDg1jRFL2dN
57ajiioAeZDZmFw1/cgLFiEEkOAr+wP6oEcU0dPYdUMVfvOxK+kAAOGdAP9wn+cI
pO75/ZJrtwCON7d6qZcZ9i7mu1Q5Zb6FyHrEaQD43PGVDZkNTtaTTgY4xnBjUr1Q
dGTt8Eief0QuwaVuBsYzBF4L4QAWCSsGAQQB2kcPAQEHQBdBVoZzzJKwoGKeezNB
g+c3iT5CqwpVnuVIDV91dSq6wsALBB8WCgB9BYJeC+EAAwsJBwkQVp9fa/uVxURH
FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnzmjmSYYuZSx9
sTMeUO8OMRzj7iPRyd/SNdoX/ixCFykDFQoIApsBAh4BFiEEv2gHEBKOa8yyJoFU
Vp9fa/uVxUQAACTbAQCMrzPWaCJCWoIpYqilw7jNxMBqvIy2d1o1BWj8N8555QEA
yYG0wX4p2DAj+ryycZ9y/POqIDnIgYlwzVo0DJkCeg7NEzxjYXJvbEBleGFtcGxl
Lm9yZz7CwA4EExYKAIAFgl4L4QADCwkHCRBWn19r+5XFREcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcme4+iFL+cquSeo+b/p2YaLWqJKAijic
6Yu16sXI+QWHwgMVCggCmQECmwECHgEWIQS/aAcQEo5rzLImgVRWn19r+5XFRAAA
rZ4A/joE4JRGwMH7QXt9n0v/PRY+xNeTc62PnAEyN2IpOEi/AP0QOQhE6XzFUOG+
W/UrkIg9vbktk5EOkHyd4Ak101Y1DcLABwQQFgoAeQWCXjS/gAWDCWeaOwOFAh4J
EKdRp6BTKGO6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y
Z6wBIsYeTZS8tmX3//2NOra93WBnWNx6FjuiEnJkrU3mFiEEIZqrZhyKr0Um28Ma
p1GnoFMoY7oAAHPJAP9EKb4+0x+IeBNGSfH5hobQ7I1GPlJuvAT6SKHz0sq4IQEA
vHIIW+xrByvwnhrX7X3dMSKGqwbhGJcSTsHH5sZNoADOMwReC+EAFgkrBgEEAdpH
DwEBB0Cptk293zoT2iuSST2yWmxon6aSgGiJ/OrnuFtNO6xNQcLAvwQYFgoBMQWC
XgvhAAkQVp9fa/uVxURHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w
Z3Aub3JnFHJ0x6RDawJeN7JwrJ3Cfhh7wWCnGkDn6yNbsbNQj2kCmwK+oAQZFgoA
bwWCXgvhAAkQ5q91RF7gAoZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p
YS1wZ3Aub3JnLvJPwopgkycqUSXzBTYYrBIHcqNg5Pv9D3odTLOtPyEWIQTLpOHu
eQFmZKbTuubmr3VEXuAChgAAy8ABALr8v3m5pUoykqPyzZZdVm4qlq0kAtepSq+x
5LpQykRcAPwICbC6BwpU7v4GEcZTWpTtel0IxNSU4XQ3W6WNOMSGBxYhBL9oBxAS
jmvMsiaBVFafX2v7lcVEAAA/RAD9EiA5uaBj2YSPE4Mbo4xKRCI02bEdmFYnYpiB
OAX/+aYA/RShkFupW3ToidZQQptPk0DW1DjdJawamPKbOmz11woL
=RvKX
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,111 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEXgvhABYJKwYBBAHaRw8BAQdA8y8LlHJUsnwhFSABHa3yF1nyAFealUz2kXNY
XA2Ocw3CwAwEIBYKAH4Fgl5a+wAJEPKMR1QPosOzRxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+thHblIkK5D/QLIZJJj/Mm9DGMxmC/mdlDK
WBr0vBCxDh0Dc29tZSBtZXNzYWdlFiEETNhzf3bCuJfE8Fjb8oxHVA+iw7MAAICo
AQDA+uUFrs+GBvyTt04m6XZai5ldct0sC4xErmLFMuS9UAD9FYz7oIXutX4enk3w
h4ZIZy77rSJxymz5Zjwl9T3nGgnCwAsEHxYKAH0Fgl4L4QADCwkHCRDyjEdUD6LD
s0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmceySxlNKj+
C239rumCpdwwBOAZ8vg7OIELRpPsY71GoAMVCggCmwECHgEWIQRM2HN/dsK4l8Tw
WNvyjEdUD6LDswAAYQ0BAMv3lxpsOhKpQSnTyzcstlz6KO6esffGO2/UWmT9g72u
AQDFNtdmt+68kL+xyzYC6NjakyY68/IWQj+5ZoOrRY6jDc0RPGJvYkBleGFtcGxl
Lm9yZz7CwA4EExYKAIAFgl4L4QADCwkHCRDyjEdUD6LDs0cUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdih74hXce4aUP356vH41nuVp1iwZaE
vxGdhQwgQxSEhwMVCggCmQECmwECHgEWIQRM2HN/dsK4l8TwWNvyjEdUD6LDswAA
HiEBAIc0/rp44l4+Era+BiK3YV2A34quhXe2cwKnVdhQNEqeAP9JVDaGO8MPqgaQ
WTtiPauB6Hv17wKHjsE55ETYr8TnB8LABwQQFgoAeQWCXoPZgAWDCWeaOwOFAXgJ
EBc436uGh4JiRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y
ZwNcOTuNVCIsPtlMm3nLYgkOAeY6b61S9yHdn4yPhKm0FiEEZgN/mLREu6/f6Y6H
Fzjfq4aHgmIAAGhuAQDjCDll4c9g/+yzEziWnL/etMC5gu7WKeBgLJsqldrFrgEA
yt8Ff2L4SrVhn/inohDKwP0Kh/Zu0qUS0LItMoQqugvCwAcEEBYKAHkFgl40v4AF
gwlnmjsDhQJaCRAXON+rhoeCYkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1
b2lhLXBncC5vcmcqC54B5MxUjdUYFYc1G0z3IFY9bl9hVmGZYzALO7qunxYhBGYD
f5i0RLuv3+mOhxc436uGh4JiAACi+wD+KH+AobqIzKRgsTldN+0ZUZneU3Age13E
ZgAaI+Y4YssBAMfKO19CuESpUIBUy08T791o+cbxRgidVfBeGMUl95cGzjMEXgvh
ABYJKwYBBAHaRw8BAQdAe14y4wbsugSXTCYbcbtKabDb14nH14K9TBcEfCtAZkXC
wL8EGBYKATEFgl4L4QAJEPKMR1QPosOzRxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
LnNlcXVvaWEtcGdwLm9yZ6vBgjxxKBwIjqiNrmmJIZtFkwPD4IJEqIINOzhfkXuQ
ApsCvqAEGRYKAG8Fgl4L4QAJEBwHgaRywjVWRxQAAAAAAB4AIHNhbHRAbm90YXRp
b25zLnNlcXVvaWEtcGdwLm9yZ2n0ohIR9Kt6y2F8OtH8MLzO6Dgjv25Ca4eXH5s3
qlDTFiEEFrmn+aCTonj3IYT6HAeBpHLCNVYAAJb5AP0ULxpF8Kh+QlYqiYojpKYo
mPTgZqkbc7JY0KwHXNcHkAEAogL9yMeDnbjZiPUWODlf1ED41HprrbZZN7ADpZyl
2wgWIQRM2HN/dsK4l8TwWNvyjEdUD6LDswAAb3oBAJ5b/ObUGFEcE6J0lk3ldvGM
JEVpF+g6bllozfqdxNdiAP9zL+8yZTrNO9UjKrZoe2n4gKJ2kNQvkydmmzl7Zcbv
CcYzBF4L4QAWCSsGAQQB2kcPAQEHQDuz70xSHZdmOlPFOhT7+bGAt9dWJdul3Z6u
BAuC0HpiwsALBB8WCgB9BYJeC+EAAwsJBwkQFzjfq4aHgmJHFAAAAAAAHgAgc2Fs
dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn8ei4XbDg0jU6Zxg1qs8L+xWIEzff
3a9efsZdcP3ylR0DFQoIApsBAh4BFiEEZgN/mLREu6/f6Y6HFzjfq4aHgmIAACg8
AP9jqokwnvBNhm7ObAwC8TdxoxG6mVNhv+uon1WD/PDO6gD8DpzHBrgvuvS87eUA
elkuTOJN9m46udFoN0C8iMdLagfNEzxhbGljZUBleGFtcGxlLm9yZz7CwA4EExYK
AIAFgl4L4QADCwkHCRAXON+rhoeCYkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
ZXF1b2lhLXBncC5vcmcKzsyguAPAEjTm2nNpLqC7EmR1ZtTbaOTUhhJKPx/XPAMV
CggCmQECmwECHgEWIQRmA3+YtES7r9/pjocXON+rhoeCYgAAIpsBAOIzZ5SLTu8p
1n9XT02k6p/IHUraayDaS4e0xQmAvbsdAPwK/lOe8mmLGb1KyAmCeHire3KInouF
xS9B9K8Jl5ZUBc4zBF4L4QAWCSsGAQQB2kcPAQEHQJOMaGWUZQ2Y6+3ARQfgUgDE
sYz6KvYsYdE1H3kSmh4cwsC/BBgWCgExBYJeC+EACRAXON+rhoeCYkcUAAAAAAAe
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf3uhQtGFUerlk/p/WJ3Tzi
vPwvVPXq7/BY9V7FyxHdWAKbAr6gBBkWCgBvBYJeC+EACRDKRM4YdYNlNUcUAAAA
AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcZE0a0itWnJUAQZasL
28KRz3sU2PVZjQT5a78B3AlU4RYhBGRcBNbLOgUQmQjPsspEzhh1g2U1AAAwwwEA
rIO7Vjipyn97PaQGFOVQqRbKMtC6vB0yfurnUDdnCd0BALbDpRjMmqN/mVTjN/7Y
PwqsLHlV7w7jiR9b57zH4aoMFiEEZgN/mLREu6/f6Y6HFzjfq4aHgmIAAN6yAP4v
8HYJT13wI1roqkHa2VPALr3/z5ZH+b/mxdkqVqcGvgD9FGvSIFTfLkM5xsT8UkMN
eCmwW2fYiPyRHEbwmwrU4gLGMwReC+EAFgkrBgEEAdpHDwEBB0BJht+5DDAvd77f
DWYxC+RJ0fyO8iZNatnhsQO+NgrRcsLACwQfFgoAfQWCXgvhAAMLCQcJEM5XD5uM
fcddRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6yiaf2l
2haGAYCOoJF7ZuJoiVf08U3xBg9TtMMofID7AxUKCAKbAQIeARYhBKtOP47ou9NF
l1TXWs5XD5uMfcddAAAd2wD+IuZEOBO+5jezmegAic3Ix/qEdDaP0xXXuG7Rm6ke
PPcA/RnxkInASIIKTLgJ0z+cwT9prNp64YZIR1JEBvKhjKEGzRM8Y2Fyb2xAZXhh
bXBsZS5vcmc+wsAOBBMWCgCABYJeC+EAAwsJBwkQzlcPm4x9x11HFAAAAAAAHgAg
c2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnsBeRfaZEbj5jZx99YWTSDpJd
+Sx0fQpoaPgephSzaTADFQoIApkBApsBAh4BFiEEq04/jui700WXVNdazlcPm4x9
x10AAIQZAQDLeJYsYektiJlW11IqBK8S8bsQbgdhNlrMUSCeatkdOwD/X/tBYCTd
CEp8MS/J4qrorSLKplbgO/hig4ARJQ7s7QHCwAcEEBYKAHkFgl40v4AFgwlnmjsD
hQIeCRAXON+rhoeCYkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn
cC5vcmcH0LDJ6sbEY7dQUDKcv+yi8PWvNIgeMIsx3ykv93pXlhYhBGYDf5i0RLuv
3+mOhxc436uGh4JiAADJSQD+PI0psa36UL1RO7WHbaNwgWQUyYckSwKcpwmr8mOZ
vm4BAN/fqcGTO6AtOlGuHhKQH/c2Fk0dNQXF9Hp59ZD0DNAJzjMEXgvhABYJKwYB
BAHaRw8BAQdATSzlqzTrECBOHXsEicoRL032B9etxzKyAXstSMGcv7nCwL8EGBYK
ATEFgl4L4QAJEM5XD5uMfcddRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv
aWEtcGdwLm9yZx8It8bhfabIAMFdbZOurCWBmlTURyCDlWi8mVuR7y80ApsCvqAE
GRYKAG8Fgl4L4QAJEOJv5rkociaORxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZxlqKSUS+dEMoKFNBcldS47ZV0E/4Twq5+p3msD2eTirFiEE
so/CHBIV0OmCxBKK4m/muShyJo4AALwqAQD25rC/mnhH09l5ivXZR9GspgPJ7KU0
8O+ACXaQ5712XwEAybY5q2sBVk4/PttKN+bzzsbxeEmOtj8vmQhNAE+WmgoWIQSr
Tj+O6LvTRZdU11rOVw+bjH3HXQAAxdYBAL76gcz0uV7HBZ//fLF0PSXHysBH0Wt6
XPhEsL+70M6RAQCFquFekEv07svx1NzUYKsHvqWum9xtoounNQzhBbBiCMYzBF4L
4QAWCSsGAQQB2kcPAQEHQDLRO/CGbiae652WLT9fbwc+MoCCqGtFoKOT6hPlspw5
wsALBB8WCgB9BYJeC+EAAwsJBwkQ4k+7G5+tyZlHFAAAAAAAHgAgc2FsdEBub3Rh
dGlvbnMuc2VxdW9pYS1wZ3Aub3JnYUdPd0PzfWEYp9ZzD1vmO1ko9VuWN/2GA4Ws
9lOTjHQDFQoIApsBAh4BFiEE32pEDtnecjsOvH9Q4k+7G5+tyZkAACfkAP4u3Wj6
Yrn7/zVRhd34+j7qUX3Z5snDr1wWp0NGR62TWAD/eJedIfzmPlWloQ1ND4Oz5USY
sopmFMy3my/Rkq7ybwPNEjxkYXZlQGV4YW1wbGUub3JnPsLADgQTFgoAgAWCXgvh
AAMLCQcJEOJPuxufrcmZRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt
cGdwLm9yZ6UcXqrxmqmolg3unKr/fnN9QL0wPEzjHNX7a6qlIcgfAxUKCAKZAQKb
AQIeARYhBN9qRA7Z3nI7Drx/UOJPuxufrcmZAAD63gEA5T4cxzzNwM+IDNEyFU6a
Moi0zD2JsLWH6Gq1bklb+dkA/RWfWWBgLrPg07CtKwXLv684Aw0KehSU7/k8M+kW
xcQKwsAHBBAWCgB5BYJeg9mABYMJZ5o7A4UBeAkQ8oxHVA+iw7NHFAAAAAAAHgAg
c2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnUs2cPBm+gFQsOvLNTt8k2WKa
p5TMWlrvkjmoZksuZCQWIQRM2HN/dsK4l8TwWNvyjEdUD6LDswAAidAA/AulYkp5
ge98uccUXmvi3MvvL9/GZB2m95O/253i75OfAQDstfI0MSL2jhW3+gws4s25dFPb
swpYeYth9TyURIXMBsLABwQQFgoAeQWCXjS/gAWDCWeaOwOFAXgJEM5XD5uMfcdd
RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6LJ8vsZFInJ
L2/6GhdYmYXypH68g1AQ/ItDp1g/ioW6FiEEq04/jui700WXVNdazlcPm4x9x10A
AOXGAP910RqShZzWprynjZwOs+2RAtTOdLAHaKWPJFPhRU4+0QD/QrKO+SedH1nK
skyw+uPVR2WT1Ch1n+lKduh5fDjYOw7CwAcEEBYKAHkFgl40v4AFgwlnmjsDhQE8
CRDyjEdUD6LDs0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v
cmeGRHfsGr7bFc4IK0V1is+Cu3oQ8/8L4o4R5hK+/lpf8RYhBEzYc392wriXxPBY
2/KMR1QPosOzAAC/4AEA0HC5Aa+39nAl8ie7fVWJUHJ2KuLI7qkQQU3k9aY8ajMB
AM38MyyKu84f0Ff8TafFVbaNsip3OovWiX+c5EM7ed4NzjMEXgvhABYJKwYBBAHa
Rw8BAQdAr6StRwze1t8ycBcEB/LoR4N8VoxPXi9hSPyW1PHeJw7CwL8EGBYKATEF
gl4L4QAJEOJPuxufrcmZRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt
cGdwLm9yZ/Nji46QO366570jXS8Qi6RdGIYjZ5SjyEdJG20qWlO5ApsCvqAEGRYK
AG8Fgl4L4QAJENIm805wBkc6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv
aWEtcGdwLm9yZ9G67uBJ4ifXTaBkgaGY4TG4LeRSQHbI1ma3nwsPrCG1FiEEARSn
ZpdvRWoe7RdT0ibzTnAGRzoAACSdAQCy56dQFGaR+TF1NBK2N/3fZe1JZ4mbdr6r
5qpcyanIcAD/RjZMD4Zg6eW4ltTG+8eENozwVzNfxNPG8m5QUJUR3g8WIQTfakQO
2d5yOw68f1DiT7sbn63JmQAA1xgA/1LUIQ7LHcPZZAx/Ab4xq2mA6p2zgk4tdS08
/1V3u+RXAQCv5JJWBfkFnSkIFnj/lTGm0OXGSqfy1CDiBM4nw+iwAg==
=KuPt
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,82 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEY8kujBYJKwYBBAHaRw8BAQdAWTNBNzb2n4kEsVt5MnuxTlHtoKVatVpguukL
zByL1NfCwAsEHxYKAH0FgmPJLowDCwkHCRACPMAZc+2d80cUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfOZuykxfZxcuJmM2CQdrLhaX1L56gT
VRt06gWe92cwZAMVCggCmwECHgEWIQSaGuk3tcuLxGBIq2MCPMAZc+2d8wAAD68A
/jpczJOkjNhtGi1Zmer13q1Ufa3yoSM5Ah9+GnqvtU44AQDr3DWXRfXSiDV1OIRR
ZApWuJIh6qTYFgi+vdXVfYknCM0SPGRhdmVAZXhhbXBsZS5vcmc+wsAOBBMWCgCA
BYJjyS6MAwsJBwkQAjzAGXPtnfNHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx
dW9pYS1wZ3Aub3JnDB7h6it/wA4hVyg7GEZdcMgdRC3qMrQROz3Gz9tRDOcDFQoI
ApkBApsBAh4BFiEEmhrpN7XLi8RgSKtjAjzAGXPtnfMAACcZAP46dblIYoHnyLmr
0i+D5diKR4jdLhwXRWKOZAfxjIe6lgEAr8/0DUUYWEGgqgjS/EsVRl8Plf4kvvsc
4gq3m9w5HADCvQQQFgoAbwWCY8kuyQkQl1V90UfZnJdHFAAAAAAAHgAgc2FsdEBu
b3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnfA3OtnGbuuceG0sNijXElTPlJdBD4lWL
+hvesdXDeiQWIQSrnvHIljFRmELtVZaXVX3RR9mclwAAL/sA/1KK+ZTRbb4wo/j7
vVMd43pUNLxMdH/IXa0wbacR2qTHAP4/9osbkWshzvGEEsz1zNmW4+TRNHZPcdWZ
PuUCeKb5B84zBGPJLowWCSsGAQQB2kcPAQEHQPSd6esOsGVxKMhuE+YcnetwK+z0
QY+rdzwchYvnFcVfwsAABBgWCgByBYJjyS6MCRACPMAZc+2d80cUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdbLv9YW0qYCTdo+AuOIGwxFwDQ
w8NZby2iZXnP/VzRjAKbIBYhBJoa6Te1y4vEYEirYwI8wBlz7Z3zAADp9AEAsdNW
IDf7Wa3r48F+9nH66yk/tj+ZYnVVcoFDNKPXEpEBAN8s3XLBpHCFa2Slf8PqnvOY
MfN4bUx5JlsHn4zJz24BxjMEY8kujBYJKwYBBAHaRw8BAQdA4cAHLeVcg9bGjBCR
tpUVDRwuQC26MvDz52gn7h+nwT/CwAsEHxYKAH0FgmPJLowDCwkHCRA8X1u+a3kM
BUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdLJosueLGr
nIfAOKmbwKtmK9XyWkCyadOcZt8TTxv2bwMVCggCmwECHgEWIQSmjfAOuC+cScJ8
x3I8X1u+a3kMBQAAmXkBAJqDDwKxwCDVzQs9d/0eqHa/T4Cauh11QpKRZhFMd5bO
AP92yCTBrL5NOrkDUIDyuYgtFz9d4Yfma3DkWoIASYieAs0RPGJvYkBleGFtcGxl
Lm9yZz7CwA4EExYKAIAFgmPJLowDCwkHCRA8X1u+a3kMBUcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdqzjeSSIWu41AoXOp/0tsY4HLgpIl/
xSBcmX0IhGjYXAMVCggCmQECmwECHgEWIQSmjfAOuC+cScJ8x3I8X1u+a3kMBQAA
ZeYA/RB9AYJG9xipHkfI14qQAkLoh/oIfDYClLrRZ82QHy1OAQCriagNJAnxLS/k
w6AokXsLwrlemsZMH1ZWlqggimxqC8K9BBAWCgBvBYJjyS7ICRDYE2C0wEiSJUcU
AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeObgWaD/Y4PsfK
S3mxeOaZdUJKyU4a7TXfElnoNwFaaRYhBLKzcSFO9xr9FuQsYtgTYLTASJIlAADF
PwEAuM5kIqG11Nqnb2rubGEBu2x741cSKV3uwjkS101g26gA/iqHRsbxEGqgXvuV
z9msWCb42Od3f8CMyS7oQUypIQsGzjMEY8kujBYJKwYBBAHaRw8BAQdA09qXxgC7
HDAwngup4nZGJSjcCAK3yRtlbHtVc52nUtPCwAAEGBYKAHIFgmPJLowJEDxfW75r
eQwFRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZxaoYqMI
oa+mNbdjnCVrdkN6ulLykBLpr+3RkV4fltHvApsgFiEEpo3wDrgvnEnCfMdyPF9b
vmt5DAUAAHTjAQDT118jtjM5WUlB96axrHiOSQlaYgNa1zYTWmf9tVHYAAD+LYnz
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
uqeYZXB6S2p/n1w9F/Wii3QO
=eBns
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,103 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYVw2ohYJKwYBBAHaRw8BAQdAPDd3I2+R3bAADmF/2PZ4ajvrV6DQCJ+8T2pi
1t7SMITCwAsEHxYKAH0FgmFcNqIDCwkHCRCs4gVdYQzqA0cUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdPNo7mSILVpMZH+6YeEYEB4H0rcL2D
8PCsIdNzvTOmhQMVCggCmwECHgEWIQTBvGeUpsYoG5aKakGs4gVdYQzqAwAA+CwB
ALYuoR7L6HP4Nf0m0rflgh9pbKUhs/l1K9VQ7tcqQnpRAQDhfmNcnUzyuH9FVjXD
g+QDnmUAuS4f2RpGom2AT2FUDc0QPGRhdmVAb3RoZXIub3JnPsLADgQTFgoAgAWC
YVw2ogMLCQcJEKziBV1hDOoDRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv
aWEtcGdwLm9yZ+0kXWrRCp7lt7AWDWXjAjZ89TIh+/8jFsWZp+iUiHa0AxUKCAKZ
AQKbAQIeARYhBMG8Z5SmxigblopqQaziBV1hDOoDAAAtHQD/Vf2QHja6xjJcS0BP
LYVAD2CRTEb/bEoVEk1OVcMCFnsBAKz/9hHy56r5wnfK4ENbqky6vAhZ/8//MVSX
k2M7r/4BwsADBBAWCgB1BYJhXDbeBYMJZ5o7CRDngGTBK215AkcUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeYN8US6eODPR7kfvRjoEeW3mtP
yt1ds41mtzt5Tr8pXhYhBJyjaQe0b+e2ue6WAeeAZMErbXkCAAAw2AD/UmzUCuml
9LvE6S/7LVacx/3lpmaDba7xNYGKpPoWTHkBAIWqEIB1CFtWsy8v6q1N4CFW4pQ+
AzCwMiPR0k3ZTqsDzjMEYVw2ohYJKwYBBAHaRw8BAQdAgEfyWpL1gySc4IgW+p4N
gkKLx+LwzzClMDZuchXJQITCwL8EGBYKATEFgmFcNqIJEKziBV1hDOoDRxQAAAAA
AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6IwMNVICHbYLCcFpj/W
Y7EUfjH+eSXwIVexGjabK6SlApsCvqAEGRYKAG8FgmFcNqIJEMIjwc6epbKuRxQA
AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZw0Rzg97+q1NxZao
qrLfl250/8b7ZccLPPa/K5z+BrQrFiEEjFhBRy/Oz8ttpwUvwiPBzp6lsq4AAKvo
AQCLlI1xvjAUZSXe8w4mbUGwU98O/F/ywfHrISK3PjXaDgEAyCMaYNSmPjXLzifi
WfHdZj4+Bz/avutycHKYfd+wsQsWIQTBvGeUpsYoG5aKakGs4gVdYQzqAwAAposA
/iHyiymQfsw5RUiuLNm2AWHrRqJ7GRyq71G1zfKu9zmlAP9bbTEKiZ5fQr1Drpg8
eu7RTEkOzsSGeT4sz+dBFi3PBMYzBGFcNqIWCSsGAQQB2kcPAQEHQEcJfUQa7RKp
hDEGVCDy70vjMaQfxX9KTZVABTVlZE8pwsALBB8WCgB9BYJhXDaiAwsJBwkQKzgl
3AKgX+pHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jnto9D
1Hi8CK1cyPuJJofGYM0OTk+QggZrd5AtHhLQF5gDFQoIApsBAh4BFiEEKipKI6fu
wRm8C0ZkKzgl3AKgX+oAABzmAP99McELRS3IpQA++u4GWKJB1Uu07K0oF/tEz8pW
wlwmowEAnsndbSaeB89gh/KsrwArxqo+hRW1+ytUamM8M4oTOwPNEzxhbGljZUBl
eGFtcGxlLm9yZz7CwA4EExYKAIAFgmFcNqIDCwkHCRArOCXcAqBf6kcUAAAAAAAe
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcu384BpSI7+RW4Z4Tf24jl
Gk8i9oh1sG1KWx2fgyXFZQMVCggCmQECmwECHgEWIQQqKkojp+7BGbwLRmQrOCXc
AqBf6gAAOP4A/1mxJe3A1TtOS5vaQWPL0I1Jtysz+NVD97QR5bZiBBAkAQCf8Z6f
JME9XnWX8TKVo8lhzqubnZkXbzfU/7+yhcvNBc4zBGFcNqIWCSsGAQQB2kcPAQEH
QImCfiaNMV+cNBvT8k9cCBogjQI+RpOSkF3Pd5ie2IW6wsC/BBgWCgExBYJhXDai
CRArOCXcAqBf6kcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v
cmeXRpEVGcRepccZOhKbeWLIxCpnOXX7x9vF8sIvDdOGpgKbAr6gBBkWCgBvBYJh
XDaiCRAmJwxe8t01VkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn
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
6tVijMwJwsALBB8WCgB9BYJhXDaiAwsJBwkQ54BkwStteQJHFAAAAAAAHgAgc2Fs
dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnjTc2/TQ7sXs9xg3bTbFCM566Io1a
yY+iMHugCLjVapUDFQoIApsBAh4BFiEEnKNpB7Rv57a57pYB54BkwStteQIAALEm
APkBGsLA1gIGyoiHsZBoSMlpM8YzXLyycTmrh8wXbvKOMwD+MLDwaK/Lk2iWP2XR
YkvjL7t28PRW5QntfmbdeYo7LAfNEzxjYXJvbEBleGFtcGxlLm9yZz7CwA4EExYK
AIAFgmFcNqIDCwkHCRDngGTBK215AkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
ZXF1b2lhLXBncC5vcmfUhcm0SLsYBE2Fo52dDQlwfeR9kewpQ30vU6/Jmt4fiwMV
CggCmQECmwECHgEWIQSco2kHtG/ntrnulgHngGTBK215AgAAl9YA/1ZkLO/pwSMp
LDoVIF0mwxmdt8dM4w2OhyC/2YQDz3ucAP9jcN+hyK5VOQSFP2aVgpuOYKdvCY8P
BmAORyXrthQMD8LABwQQFgoAeQWCYVw23gWDCWeaOwOFAngJED38FRq/rYXVRxQA
AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+NLCfmOtfrpdDHh
XaJpTr+FgUarTg/N8eB2cGjp3dasFiEEAxgmEbkbHn4guEjoPfwVGr+thdUAAFmK
AQCAe4NyivnvivrzigfqLsw4kFT0qT+bPmLYKex4S2/exQEAs+uZA/9XLtTsh4vd
3F12bsN70ocR3hxUOHdp98pHIQ3OMwRhXDaiFgkrBgEEAdpHDwEBB0BbMrenaGBh
zwHw0/KNbeIfo3xOGIw7gS9XQoPUg0gRAcLAvwQYFgoBMQWCYVw2ogkQ54BkwStt
eQJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnJkfw7t/i
0OgLq7OrweNN1Bl6R4k5Hq6+U8l4KREVKLcCmwK+oAQZFgoAbwWCYVw2ogkQ0VOh
kefqDZlHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn5Aqa
ybGveCfZO7Apy9LAnV0tTB4f6b/VgeJXiXc5KnMWIQQVTST19kFQCy0jRXbRU6GR
5+oNmQAAyNgBAIQDCVx/z0hDZXNw47jcyoY8UVtDA9e7Oo7QdIIPbXzEAP9/MRy3
ivHBtoqskLALcDwOpK6r36fDEAYfXL6FUSn8AhYhBJyjaQe0b+e2ue6WAeeAZMEr
bXkCAAD+iQEAwOh/nX9ibmwd74yJqe2XGRPkW/hJnL+hBjA0bUksnVcBAImFwwWV
0VVaM7ese03qfeFrCrATp2+BuRjWPB4rIFYC
=tJ73
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,150 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYVw7LxYJKwYBBAHaRw8BAQdAIyDryhHnCv0cRrufsE4DSgNUzf8h2vguogNv
LoNw+BnCwAsEHxYKAH0FgmFcOy8DCwkHCRBA+KAUHfJ46kcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmebEHkcRhKfvuXoxUTNrhFtz7/wO92r
d7ZHOWqZRO//BgMVCggCmwECHgEWIQTxyZxAGYN3A90XxFRA+KAUHfJ46gAAjqwA
/3xmhxa5BKv1xVd6gnSaPeJDOHjFiBiFjQF2u/SZLCanAP4o97ogoXbXVXilGXMY
ZBKJOgf7hGMdtqWoehFAXD5uDs0TPGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoA
gAWCYVw7LwMLCQcJEED4oBQd8njqRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ6rzGhaXu9mJHzXRqoHiBxzL7xhLknGovA2bo04yEmIhAxUK
CAKZAQKbAQIeARYhBPHJnEAZg3cD3RfEVED4oBQd8njqAADaWQEA1ZZZr6SuGluc
76SuirpN0Pn/xM8FGzJM3YpfnTNoQ3AA/3PXe3BcXzkpGU1nIkYUrQPBcQ11zqkD
duLxV7MNFmsEzjMEYVw7LxYJKwYBBAHaRw8BAQdA36fJe5uw6sDJi7Gg570A61wL
xi/aaeS3ZIda9KxzfarCwL8EGBYKATEFgmFcOy8JEED4oBQd8njqRxQAAAAAAB4A
IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyn7ff5e3BXFZeS1PUtmzxwR
iQv+v6LUICPfJoO1pZeOApsCvqAEGRYKAG8FgmFcOy8JEJgiRHhhsA0gRxQAAAAA
AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ8VX9AiVPRqrgmwqTZ8Z
QmkTMzh1AkFn+jocsjBbcasUFiEEcqaq90zQpalQVdOHmCJEeGGwDSAAADLUAQC9
I9WOIrK2lEJfjKDoG9Ahkz9/hSzR/CQwK/9CSVeiCQEAjEIb6O0VktpN88YABLxF
KBHzeqppuDL/bFt2nc7brQEWIQTxyZxAGYN3A90XxFRA+KAUHfJ46gAAkqQA/07+
+DkgANW4N/oCmYfn6r1ebgOU/Hpg1AjZ+qLXJYD1AQDRuxhBw+qkVYvmB22z24hQ
8erXvRgQj2WkDzKL6IxDA8YzBGFcOy8WCSsGAQQB2kcPAQEHQFf7UtrsNcdD3caO
64dTpac9KYyR6KbEv6LwP15/VjkbwsALBB8WCgB9BYJhXDsvAwsJBwkQxzgv1ihc
GPBHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JndkeHXkqW
dLQf1scTYWce8AdkW/C/SosZ1phhknPvAAsDFQoIApsBAh4BFiEEYsV9kNrSU96g
HVqGxzgv1ihcGPAAADWFAQCORCF67zFuoHc1pcP445w0m0Br28vhK5dE+gztdpHk
dAD/cajZ4shmmLZhrKUZGzbWUuuwknT7OmWdCHaUcrhuJgnNEDxkYXZlQG90aGVy
Lm9yZz7CwA4EExYKAIAFgmFcOy8DCwkHCRDHOC/WKFwY8EcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdOosKM0EuwOie6/qnp4Rq8hDBrTDki
FiJZ0byBUrRn9gMVCggCmQECmwECHgEWIQRixX2Q2tJT3qAdWobHOC/WKFwY8AAA
a4kBAKqN8sVPTul7Mr3D8zR2cK40ABq5H1LOIqMQv3VRuYPQAP9m9ENyyMK2q24n
pwGkbMI82EWrNlPezsR6CX7sOmrGBcLAAwQQFgoAdQWCYVw7bAWDCWeaOwkQ7/ZH
fT40jXFHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn0Nzm
M8fdfd0x38KrY0K3x+k9dEPD8WYPuED6ViRZh7IWIQRvgpFChCCrU1drq0vv9kd9
PjSNcQAAC4wBAIOeQBmNI7C4dVPho7W4j/24NITB5iPnZhz5AXWqHlLOAQDV30ba
JqwTp2YuMdXzLcJZcbR4g8J6TkJ9ryF3XzYyBs4zBGFcOy8WCSsGAQQB2kcPAQEH
QEEF7A6+KRKngK6vzBF2Iw50MAx1dQGyumu7btTdivuowsC/BBgWCgExBYJhXDsv
CRDHOC/WKFwY8EcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v
cmfvlrExrsc0mxi7yNHFHNddHFUDx/KpZ/goN8MoYd/NnQKbAr6gBBkWCgBvBYJh
XDsvCRABrHKY4dzSakcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn
cC5vcmeOR43iKs6y+xnfczlKmqKsF75Jm0BMc4Lbmz+XH+UOghYhBOC2plUY2qJr
r5XDLgGscpjh3NJqAAA3NgEAp9elQa8+X1NdcGn3joBG5SX/xfhfi85HhP2ZnspH
NCMBALneMvxuRj6T0gzh2Fg3TzuF4VjUduBoidkYZKJEkOsPFiEEYsV9kNrSU96g
HVqGxzgv1ihcGPAAAP9nAP9hge/R2WIH3ZZ1Oan08t17lIFtxRxlX53T+WB8gLLX
owD+L/e3jstgZZxOmiHpv+QAFSmtvVyW5E/yA+LTEfcYtw/GMwRhXDsvFgkrBgEE
AdpHDwEBB0A881dSIh05SjlDy/tkjV0wohfiWDYeXLuMB9l6kjiPUsLACwQfFgoA
fQWCYVw7LwMLCQcJEO/2R30+NI1xRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ9Rf2SR70BOzcN9N6xtSKdpDCVrcq2GaUmPO9bSYLYaIAxUK
CAKbAQIeARYhBG+CkUKEIKtTV2urS+/2R30+NI1xAADyHQD/UJW3EAXhev3HcJot
SvzXgF34SwG6VGTmQ3wfM32IlrYA/19XEU1hXCbjgVcLhbq6GxoSSoB0t+KRkEyp
VifQfxIHzRM8Y2Fyb2xAZXhhbXBsZS5vcmc+wsAOBBMWCgCABYJhXDsvAwsJBwkQ
7/ZHfT40jXFHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn
uoHfw/ZDlQm1GeAifFgG/VtrZBFLV7MTLmjS/nwI8+0DFQoIApkBApsBAh4BFiEE
b4KRQoQgq1NXa6tL7/ZHfT40jXEAADF9AQDf8JjBMapdY1CexnjT4Z9KdwOx34mL
0hdML2FOkWcejwD/cvid69hS6BAv28fouue1z4RDoNQ/2EvK30JyZOsgSQHCwAYE
EBYKAHkFgmFcO2wFgwlnmjsDhQJ4CRB3szLkERRWy0cUAAAAAAAeACBzYWx0QG5v
dGF0aW9ucy5zZXF1b2lhLXBncC5vcmf0etZmDtWmQi+vtRfyHmM4M9c0aZioL8x8
4LlLWTCcJRYhBFUoueXa/FGe0uN/A3ezMuQRFFbLAAAdLgD3W/pSxErms2P1Ew+V
/HYoxr/tMV8z8DS1fta8E5Gb8wEA0J5DyUXLL5BvsUh925TiS+U49zMorP2Gj8hj
EVQdCwjOMwRhXDsvFgkrBgEEAdpHDwEBB0BrmR/IH2qeJi0iFAbimQQJWeynEVpg
LxCmXkklEoLvwcLAvwQYFgoBMQWCYVw7LwkQ7/ZHfT40jXFHFAAAAAAAHgAgc2Fs
dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnkXBf3P7eJJ+sLvrPB0modP9Zqcwr
GP4mC9oaFEqiK/gCmwK+oAQZFgoAbwWCYVw7LwkQ+UG8hD57VgBHFAAAAAAAHgAg
c2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnNvMYjrc9iG03yPPEagLgU8qT
fd7+1N/UeT7kONbqb0gWIQTkTvowOLZhDN9KT7P5QbyEPntWAAAApBMA/jKglIgk
Nq29pk/ZUsy2ydx4K4ConGWJmmlMQljgvhJ4AP4lSYJXNQm5DSFWiH4ohxARu2Av
2Oe5mCf6vyNh6MSEDhYhBG+CkUKEIKtTV2urS+/2R30+NI1xAACNpwEAsYBB30Oc
O2Hi5o8vhO7XkJFA+c/3G/ck9hxynz+HgGkBAMIAjwJ4Tw0Lwn99nka5WcPWE+8E
bx2mUiGuitDbwWYOxjMEYVw7LxYJKwYBBAHaRw8BAQdAEfjVcN97e33nOlZlcVeq
D/Qd1c3JHn0Ixro9D45D29vCwAsEHxYKAH0FgmFcOy8DCwkHCRB3szLkERRWy0cU
AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf/WrI/Pu4CPera
wA4RU97PdOPkIW7DQagBB7pRkbGXuQMVCggCmwECHgEWIQRVKLnl2vxRntLjfwN3
szLkERRWywAA50QA/1Jnw+pWYgjPcWQjcv2MUiowLD9G3GQ9OtLmB0LWAD0pAP9Z
+WGHraSzX6R0FbTCPTVy3ve+sCM0CsBAUPu4+xh7Bs0PPGJvYkBvdGhlci5vcmc+
wsAOBBMWCgCABYJhXDsvAwsJBwkQd7My5BEUVstHFAAAAAAAHgAgc2FsdEBub3Rh
dGlvbnMuc2VxdW9pYS1wZ3Aub3JnyHe3qxnnB2maicyjGH5rJcHZItL981PbfICu
/Dfp7fEDFQoIApkBApsBAh4BFiEEVSi55dr8UZ7S438Dd7My5BEUVssAANYaAQC6
6zF39XZW0ecSzPho8wZvrmkVq+tj6twPcSbmDrMTAwD/WG+rvz5DbbDWnGpfofAi
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
VaHUP2GjcbiJK8DC5VnFY/lYJZvCwL8EGBYKATEFgmFcOzAJEBjLK9plRl8DRxQA
AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ4z2SWv5L2JtFSDk
5vbQL0Nv6TSNStwz/wSIxgj/iKiCApsCvqAEGRYKAG8FgmFcOzAJEMAbtiqrB1Cc
RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ23sxfrXTzLt
FupeU6YELCUKe+Q34qyQaEb0HvXJDVvtFiEEk+4dT4DNa7eYAxiMwBu2KqsHUJwA
AGwDAQCxQQdJBIA+EDFu2jCLQkS2iXR2eN/f8saaIRAL1LDW0wD/YcZvgAnz53Km
6GV1dhLMG3ORUCSquEuI4ziZA7XuvwQWIQRb7j1B+Fsvy8MA3k4YyyvaZUZfAwAA
woQBAK2PwoWiBSikLnSXMHnCjuqxSSGvhgv7UeVeCpq+7tDAAQCiZr/8xobWvBcT
fusE1Z+gEM8lr7Qp3BBYhFsabUIvAMYzBGFcOzAWCSsGAQQB2kcPAQEHQKmqY1TU
33xTHcKp7wGEYygNTFFzSa+NuYgdeJRf39IbwsALBB8WCgB9BYJhXDswAwsJBwkQ
jwSP+DsXNQRHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn
+iFCJp8aCPHKKpfzQozgBqoLFMism9bpah8XtW57bdYDFQoIApsBAh4BFiEEDpdN
CsugxNj1HXz2jwSP+DsXNQQAAG/VAP4hOYTPP3p4ggHLPrs8I05rrhXdLfdqvd8E
Xzo463ThEgEAkZ59YEv4xEQbBnZQNPfPBp1dLCb9WS83MAg9LRfNbAnNEDxlZEBl
eGFtcGxlLm9yZz7CwA4EExYKAIAFgmFcOzADCwkHCRCPBI/4Oxc1BEcUAAAAAAAe
ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc/AuhzWxwGYUhK+fE6MAif
c2G8djR7o0FbJI17OUYU4wMVCggCmQECmwECHgEWIQQOl00Ky6DE2PUdfPaPBI/4
Oxc1BAAAJaAA/3vhsOIIjwtUBlBe9sx0CML0Ch9SgpZMDdC6VzWgP7iRAQD+mok5
ZrUYQtbqz7xzEAlC6K768SOFOmVo21rmJDjADMLAAwQQFgoAdQWCYVw7bAWDCWea
OwkQ7/ZHfT40jXFHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au
b3Jna8BzUAwUdJXYkOdU9Kflk8RjN0jLr7Ph19Q2F0fYAC4WIQRvgpFChCCrU1dr
q0vv9kd9PjSNcQAAdhoA/2Vwt4OX5WDOPOkDwSDFU5UySLcPUzXvvF1VFpMPqT0F
AQCg3hKjNjnkmtHLhfp2vIbJU43AtX6npGX31viZM5gcD84zBGFcOzAWCSsGAQQB
2kcPAQEHQKK2XxyLVReZiqBzQ8Wj8qGK7Y32LDvlomwjzfleTNDswsC/BBgWCgEx
BYJhXDswCRCPBI/4Oxc1BEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh
LXBncC5vcmetVpolfIqsjdAEVj/L9JpR7gBC+K7HOUsomTZFNCUTCwKbAr6gBBkW
CgBvBYJhXDswCRDDDVwDNJyG60cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1
b2lhLXBncC5vcmeTyuXjqoyVot9y+zmEaaHyi1jBoxwjkcTRYwdiIS9MUhYhBMgq
6BVZ1+ywJrEt3sMNXAM0nIbrAADo8QEA/HKxae4ZaHYFP5fZJO6HVBqqLViTdmSv
NIuBgS1mXTcA/Raoi8mNjtWFllYUXMOjjcg68cDNiYKfq5ei3Gs51vAKFiEEDpdN
CsugxNj1HXz2jwSP+DsXNQQAANZwAQDeOl8WG6TodPKCfkiuCWezk5Q3xnkqQ/nu
/pWAH4JpMAEA3tnFnt3Zt40lmY2qtKsP5wTOw04fwfvxUev5kYl/yA0=
=bvGd
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,71 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYb5eGBYJKwYBBAHaRw8BAQdAU1nm6ngUIGEAwJqKHfRU83nPipGZWJawUd2o
ZnZKnCvCwAsEHxYKAH0FgmG+XhgDCwkHCRBWK5re5/eJ9kcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdTFuOqygaXJ+MtCylqsUyPftVPHEYc
TzSFIo57cJjkKQMVCggCmwECHgEWIQR0MsEjdhuU7FDVDPZWK5re5/eJ9gAAsEgB
AJaVBY42rWGwxuW8jWi1r2iZztCAHFp5iFLOg3N7yFJSAP93JIHBPkUWpXDxhwpb
TxaLiyU3CB5fPxrk3UMS7x8UDc0TPGNhcm9sQGV4YW1wbGUub3JnPsLADgQTFgoA
gAWCYb5eGAMLCQcJEFYrmt7n94n2RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ4JGKTVlZ/XToSXrlJdySpJmkWVtANeCMgKlDXlbJt4KAxUK
CAKZAQKbAQIeARYhBHQywSN2G5TsUNUM9lYrmt7n94n2AADlAQEAnR6msNsYfyyc
+f+4cF9xY8zmthltGdH5monJ75EE63oBAILO+WS1m033RpKo7PCz+R0A3r/1JlFf
xT9aTLsawPUMwsAHBBAWCgB5BYJhvl5UBYMJZ5o7A4UBZAkQnehn5spqJ1ZHFAAA
AAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn1Fe/1KSDN6ysQHt8
JeONCHcVmM+04X5DFOE/PuMvvMUWIQR0dnxPKxX1fzOU/Kmd6GfmymonVgAAYNUA
/R2fw0BYl/nJQ728PpfEjBct877YSWc5gLyXFiPjcOoCAP0X3uGSeCYQ6sx6hU37
DWRXdwO5TStUlrPufxK4xqFqBs4zBGG+XhgWCSsGAQQB2kcPAQEHQJdSHuo9Rski
e1lg7qfTsFQqdD8ahdzvjJyyLKTlVCeawsC/BBgWCgExBYJhvl4YCRBWK5re5/eJ
9kcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcMWIYlm0sz
su//dPXX5+P+wPSLxBjTZyeJjbnsKBa21QKbAr6gBBkWCgBvBYJhvl4YCRDysLla
pFSI2kcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmegmG9+
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
Lqf39H1ma/rXuEebYHsrScSQtQA=
=Rfni
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xVgEY7191xYJKwYBBAHaRw8BAQdAt3Jnair5yJx4QkkXlq/BL5OaIv3CNOChNkJS
k8pfhzQAAP4thMBQGCbMFzftJi5cCq93+QHkRPfvJjsCqqb1X2GngA97zRE8Ym9i
QGV4YW1wbGUub3JnPsKQBBMWCAA4FiEEIxvEq52Mq4bRYizgLAzlVJmO7NsFAmO9
fdcCGwEFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQLAzlVJmO7NufhQD9Hs3h
bdIz/FipSJcWBwl8kSYIowh9XlvhgYMMBayfoegA/RKnRMaj8RlilQl5gfyUhM9H
BIZcjl6sekRukxbDiiQIwnkEEBYIACEWIQS1+gibp2/j4X3BFmCWDlMoZzj5TAUC
Y72AHAMFATwACgkQlg5TKGc4+Uw4ewEApnGFy6ayTMra6FBbeoRoHdIlKJRerCvq
SkfIxsSW2R4BAKnHPFqlX3XcGIM0krsw4lpnHlxISjjMgFcsoT4YXAMCwnkEEBYC
ACEWIQS1+gibp2/j4X3BFmCWDlMoZzj5TAUCY71+oQMFAXgACgkQlg5TKGc4+Uz1
AAEAh/TpOxN4AZrYmfl/eA+McYCAlt+6xLwVIZuZwd0dn20BAP2Tj2pTFgOhUpWB
g5mj8Zoj8kU2xTlJ80FnexMtGlYExVgEY719zhYJKwYBBAHaRw8BAQdA87Vhi+ek
HS8TQ8xU5c2mgB1ezzzdXjDRIPOgSuCiP9AAAP0bejiDjIlbxBf+N7aKzSUpyGlg
DVoFj24Dm6CRcjJqcQ5wzRM8YWxpY2VAZXhhbXBsZS5vcmc+wpAEExYIADgWIQS1
+gibp2/j4X3BFmCWDlMoZzj5TAUCY719zgIbAQULCQgHAgYVCgkICwIEFgIDAQIe
AQIXgAAKCRCWDlMoZzj5TAwBAP9FLJeKOyd9+DnITTOSKusHUb8TL54yFSQUYhGx
t92/1gEArci0UDsVzpcddpQzToyQyHcUraQ7FoWPCAnqfpG1qQzFWARjvX3gFgkr
BgEEAdpHDwEBB0Az9tiv+Hr2aAv7aPgJXLe1tTbueOoUYPy8K2eJgt1OfQABAKxZ
fGL/qwuY4626kefpvDWAVbZ9u2FO+QzuNGO6x4zWEqzNEzxjYXJvbEBleGFtcGxl
Lm9yZz7CkAQTFggAOBYhBPq6hIWy1NW/FYKqljqBFed0+phSBQJjvX3gAhsBBQsJ
CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEDqBFed0+phSFwIBAPPjbKWpKK2EhA2B
E3pfab2F0D/RBnA3U1u4EeHn95ghAQDE5YbOy67HPVtRJ9+b4wx17RQv+Noxlsou
U9MeHvkNDsJ5BBAWCAAhFiEEIxvEq52Mq4bRYizgLAzlVJmO7NsFAmO9gEEDBQE8
AAoJECwM5VSZjuzbPp0BAN0Sz7zJPowGPe7Tmy07b5YQi4nNwSIeaAW5NsONX52a
AP9jIr9eJk35Zt+frsdjLKkZAHMwtCXL8soz4cnIb7bGBsJ1BBAWAgAdFiEEIxvE
q52Mq4bRYizgLAzlVJmO7NsFAmO9fs4ACgkQLAzlVJmO7Nv06gEA5g0gIH7BsKN5
vFhBu1jUBVQyOEwkTaELqZpEG33aR/QBAPM2PSocPKKISF7ilaELKsUS2V+WLWmw
E2U5yD8GjoML
=Up35
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,137 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYVwdmxYJKwYBBAHaRw8BAQdAk8bI3OCZQGs6/5RRr0wdXEkUznIxgIh3oLLZ
HLmWT3jCwAsEHxYKAH0FgmFcHZsDCwkHCRC8Hc/eraTukEcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcme3NNjp6ZM7I1EjMzjTdxwlz7fWnES3
4kL4kwoq+cNiOgMVCggCmwECHgEWIQQ5pHmBbJNLngRk8fS8Hc/eraTukAAAZjkB
AO44HIBtXzYNiVUGHJhp3A16Qeiw55hv9acDhUjMnL0gAQDT2eQfFV16L0/GCp9q
DharuFPy7MYVEYid9QJ6JH06Bc0RPGJvYkBleGFtcGxlLm9yZz7CwA4EExYKAIAF
gmFcHZsDCwkHCRC8Hc/eraTukEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1
b2lhLXBncC5vcmfuJdEIyEhbj7f+JMVhtlFcqjgUXEJL5/wOK7yYXmY7jAMVCggC
mQECmwECHgEWIQQ5pHmBbJNLngRk8fS8Hc/eraTukAAAt8MBALgsEpMAAtyES4FU
CmblPgV+cgRREARciTft9PbnOgaAAQDFqL1d480WU++kyitL5mUKlyhxxjXtoPtm
O6gkvwn1D8LABgQQFgoAeQWCYVwd1wWDCWeaOwOFAmQJELwQyc5KaZ2NRxQAAAAA
AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZw9BTBFY2ZZ4SmpzUGQv
b4/iWzIPaEeXbxnKen9KUDf3FiEEhdq2VxOy0Kv8Wk8ovBDJzkppnY0AAJC+AQCL
VDuN70ntwJxtjysSvzYU9qZDFjcAzP/joMh608ve3QD2NqAlAZ0CI7BgvzNigpN1
fR2m1XC0v4yezybC4CQaCs4zBGFcHZsWCSsGAQQB2kcPAQEHQHzVQ0sO6/t83vno
pQOQlopwjaHInq/afjZVDmtSJUojwsC/BBgWCgExBYJhXB2bCRC8Hc/eraTukEcU
AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf9t3J5Nd1DbTwq
kRswDnG+YmAGRlMkHfJGZNbskgXZkAKbAr6gBBkWCgBvBYJhXB2bCRBz4vrc/Kki
LkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcNVLVTxaXP
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
zYVyIe6ksXCTAcLABwQQFgoAeQWCYVwd1wWDCWeaOwOFAWQJEGd8tw/7/hKBRxQA
AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyPcvKx5CEPlPwS9
XeC/FkwuEeIXwBpfMCKQBO4+mV9VFiEEMp1ar3PccLTj3S0RZ3y3D/v+EoEAAJhw
AQDROKCDXEGeZOsEVkatgrJYSFzBGX+xU2NrM6R6CHRywQD+OzbYktS8sAmE7MOd
LjgS5RkLue2LSSfSDmXR2r6PGQnOMwRhXB2bFgkrBgEEAdpHDwEBB0B2qg/kQ5ii
ZxubFFwaFQ/vCMAMSlMWrPLR/4cc1vTqGMLAvwQYFgoBMQWCYVwdmwkQtDynf3wX
avRHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnGTMWmoOh
7alBTD9MiAmgrZwqf7hXC5iP+3ZFGRiTreECmwK+oAQZFgoAbwWCYVwdmwkQNhsA
H3EgoCdHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jni29b
7AKRh3IsUvJwGZaMkXBbYLEE3zZL240KJ5Yd/NAWIQTS2X0aQMxF7OMwBG42GwAf
cSCgJwAAlwIBAPMitwdPaDSCipP5PTpn73S6QQ/oakY2GXQQpSILoZ0sAP9ma52e
zd3Ef4MS6grWtM7YziFT0TgXJgkqXb9op2K9DRYhBKcxmpsWarUwpfusirQ8p398
F2r0AAACigEA2RCf3gA19TtoYYPUeE7wf9CzbeR0plGRhIGYxej2jGwA/RwuDeiI
aqwLN99MAY4w5akwDVZOHplXpFms8Ki1WnUHxjMEYVwdmxYJKwYBBAHaRw8BAQdA
8uA9nQIUSGk+eVWb/VEr6RMv7IO1CUzEZUseIa5m5YPCwAsEHxYKAH0FgmFcHZsD
CwkHCRC8EMnOSmmdjUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn
cC5vcmdxon0OeFIi+jtSAiPy42gVKo2hgFQYBqp/AZ8OpXPNqQMVCggCmwECHgEW
IQSF2rZXE7LQq/xaTyi8EMnOSmmdjQAATfwA/iK7Ab91rzETGQXmlRCvH1Yo7oph
PmNRa0srdiT4p/ywAQDLsJbZY9MPcVh82DxL1w/vC01zXbO0AGlEYUQDGxBLAc0T
PGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoAgAWCYVwdmwMLCQcJELwQyc5KaZ2N
RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ9+0s4dvx5ca
70dHxiLPzovre+NqtglUL6YxrnRAYF1jAxUKCAKZAQKbAQIeARYhBIXatlcTstCr
/FpPKLwQyc5KaZ2NAAAZ0gEAunPv0OLMDRsOA5/qVSFrf9ERK9KyGesjs7EQ9Rl5
liMA/2xV/X80DwKY0ytoF6Gmqxr5wMSns5ZcvDBbGJl7qM4NzjMEYVwdmxYJKwYB
BAHaRw8BAQdAW21wFQDicmTa+zseQ6C8qGXSiGs/v6rnJLgO/LsJQsnCwL8EGBYK
ATEFgmFcHZsJELwQyc5KaZ2NRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv
aWEtcGdwLm9yZ/jmilhMLNoEq8L9amKw6JuAQQses4ZsHjd3PaJZgyTeApsCvqAE
GRYKAG8FgmFcHZsJEB+OyxG/TbN0RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZ5d+CLnY3K/5d64iFHXzo8Np6V5zIUs+6S7quw7A3YmwFiEE
VIxzLR3oBVQH/bOOH47LEb9Ns3QAAJvyAQCvsq1GfYP+mHKedbX1MpiW4FbYFR1x
wzN80NpuF/cX/QD+K9CSFfMYDteF89A8oXGSaPGsK37Zs4znSLyJwgfFfQkWIQSF
2rZXE7LQq/xaTyi8EMnOSmmdjQAANu4A/A/p6c2ImEi0PWhzclre1+9/D/eg7/gI
iSUC40OiCgthAQDgrEp67PQBPuGBBpEoYumMdlDZWH55zIm6YZEKUvw8AcYzBGFc
HZsWCSsGAQQB2kcPAQEHQAXJdxpg5s179q02cQAJFOI7oont4QTI1VEr73jl1bSn
wsALBB8WCgB9BYJhXB2bAwsJBwkQ3IapfNLIGdlHFAAAAAAAHgAgc2FsdEBub3Rh
dGlvbnMuc2VxdW9pYS1wZ3Aub3JnulTVgQDVcGnq1pIWl911apVKeSdAALgwocRk
NlvWKXUDFQoIApsBAh4BFiEEJpMjfSztC7aPEY143IapfNLIGdkAAFO/AP0e2lUg
Zge4ZyYUL6mh6pZeQOyzKnzPIFtgYq5Fn0eW/gD/WaK5e4h2As7WbeEBm1KkQ28y
ST1RKDP692I1MV2k+AjNEzxmcmFua0BleGFtcGxlLm9yZz7CwA4EExYKAIAFgmFc
HZsDCwkHCRDchql80sgZ2UcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh
LXBncC5vcmcPnps0Dp5p0YJ/AjuWO0Fr+16rqp8oX6VaEy4uMPIgqgMVCggCmQEC
mwECHgEWIQQmkyN9LO0Lto8RjXjchql80sgZ2QAA4ZQBAPUWBaex2nKunWta81+h
0n1uQqFSpLx/wFU1MVxkvJIzAQCdthYV3FUMXBhb/PTEW3rfcU4F0Emm5221x6U4
sxn+C84zBGFcHZsWCSsGAQQB2kcPAQEHQITURTrPxNzEJGkh1r4YUgD8dfWAkfSH
g4MX3piLSolfwsC/BBgWCgExBYJhXB2bCRDchql80sgZ2UcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeYFfx77wKkI80lLycQaD2wVBe8sPaL
G8nbBBXidVmB2gKbAr6gBBkWCgBvBYJhXB2bCRBM3zDGnGovIkcUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdf1slPSghIYzgQ7X1X1TUUJWaG
xyrHUBfeW8jWbaEKkBYhBCqESu3tyggv1PeZoUzfMMacai8iAABZMQEAxWSNnObr
JX8g1D1WL9EqlX8iUK++B5bvMv+Lda5pPNoBAPt8p6l1fEWNKZXtWxnbC9SVSm5J
mL8asTSEDDcHZQgDFiEEJpMjfSztC7aPEY143IapfNLIGdkAAGrYAP46iX0UVZC9
9NxlZRON4d/+JHKvCrCOaDX4wTEjAFrcogEAqDDPOvZWO0HO8rR0YivR1wU0kwHG
Uujn4XGt2u3GygHGMwRhXB2bFgkrBgEEAdpHDwEBB0CLheVc9lxL6EfpgSCeVl+j
oMjxZ0TeXufKaFeEwzT0QMLACwQfFgoAfQWCYVwdmwMLCQcJEGd8tw/7/hKBRxQA
AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6nLVMH/XWlzbxMX
e/6vzRsE/gYP32ACMJftuYMSd+K9AxUKCAKbAQIeARYhBDKdWq9z3HC0490tEWd8
tw/7/hKBAACulwEA1GMTIrbA8vq2ZPFRwaw8rdGiNWhQxDi9/SNgCBu9HAABAL5u
IYWLF62cmWkkXZvnVQybWBoEWqqAo4nd6A8silQAzRI8ZGF2ZUBleGFtcGxlLm9y
Zz7CwA4EExYKAIAFgmFcHZsDCwkHCRBnfLcP+/4SgUcUAAAAAAAeACBzYWx0QG5v
dGF0aW9ucy5zZXF1b2lhLXBncC5vcmctbryiXEnpUrOLEhcNOX3wAiRbxjWRvCZf
oU4FxC86dgMVCggCmQECmwECHgEWIQQynVqvc9xwtOPdLRFnfLcP+/4SgQAAaf4B
AIQqi/mq6OzedNxO3YCqVDgnkR9GzPUoNUrm/pSvoXqnAP0VmRN/VR29b8+kDUjM
xI46Ogm1fh7vFTtbJju1+ELwDcLABwQQFgoAeQWCYVwd1wWDCWeaOwOFAWQJEBoc
9Nx/UA8ERxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZwzy
PrSNt7XRHHhvnZAPPeY1TtrtSPZXTpZgXvkf0savFiEEQ1MPkbRQ7bJpqliCGhz0
3H9QDwQAAHp6AQCa8V9L2z/hLfFFHEgwzmrPROY2/o6e2XvOUs89zVCBvgEAqzzw
NUUDBEu2nT/cYN2cBPNDvE0bJY7rO9LGV/aW5ADOMwRhXB2bFgkrBgEEAdpHDwEB
B0ARmJpTF4micHpu3jHAKu/oxdS4mjiJ8Iu1lSEa7Fd1f8LAvwQYFgoBMQWCYVwd
mwkQZ3y3D/v+EoFHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au
b3Jn4oTzk5+Is/hGYuMYA+SylOONmJzNgcvz/Dr0RWYoNqYCmwK+oAQZFgoAbwWC
YVwdmwkQ0AAL+QhtZcRHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w
Z3Aub3JnnTwrZh/CjtbFdynQbf0mAnySvtMu4wf6rzSjt6lKEfQWIQTF0s1GzpMz
cAdIbJLQAAv5CG1lxAAAiW4A/A1MFOPPEqacYtl78AQ62lKtGqd9M5Al9DwhThM0
sKXQAP9OM8vuVYiP41YSG/ut3uodHYVkHC0JJRgGyqfxR6nrARYhBDKdWq9z3HC0
490tEWd8tw/7/hKBAAA+vwEAmYbvwn2hUttyxJLVPGGTB41d7o8ezMHZmVAvfYE1
dRIA/ifzLQTJhmU4tnzAnD5xewYw9uAEcrh5vXoGPVn7+XYJ
=0tMR
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,84 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEXgvhABYJKwYBBAHaRw8BAQdAhT+prBx3w03lEnZeQpaj+S12u/rPca03CKfq
mwoG2CnCwAsEHxYKAH0Fgl4L4QADCwkHCRBzHOoJLEZfyEcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeOw7CuFaS0toxI+CHivZQG3DfGwwo7
AhFoa23nqE2jtQMVCggCmwECHgEWIQQBZyu2fktAR+Wk7ApzHOoJLEZfyAAArVQA
/308jglcUpVQmIxy1L1M800r/hkBzVrdI2ZDL+/ZJ3xYAP0QO0IUfmzbhJJ2NfMM
Ox9Q4bT3MIoL/zUGhjgs+rKkAc0TPGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoA
gAWCXgvhAAMLCQcJEHMc6gksRl/IRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl
cXVvaWEtcGdwLm9yZyDvQJuJzCsisCLmdrLXbsCpRM/UiS9bUNNedWA0YM2tAxUK
CAKZAQKbAQIeARYhBAFnK7Z+S0BH5aTsCnMc6gksRl/IAAAjTwD9EYpc/21MNmLZ
8yD6l7LwI5fzaaOGgzQzFQRFIOkN9NoA/ikhz3XnnwK6526jDc2uFetHvtmCZToL
Vpt3R+YHoYMMzjMEXgvhABYJKwYBBAHaRw8BAQdA3WF+/meEa42HlKtzJ3lwzTYM
hhUzptBhD634osiwuwzCwL8EGBYKATEFgl4L4QAJEHMc6gksRl/IRxQAAAAAAB4A
IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ1cJsO9arW70E7gz+dbG3FMh
sbOx1jagrd6xmcT8cVLQApsCvqAEGRYKAG8Fgl4L4QAJEGFKGCgk1kTzRxQAAAAA
AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ48eqVw+tBIAHCld8kRT
ZsCvRW76A3fR0B4xkc8qoNXNFiEEp/X9b1/I9HtQVKjCYUoYKCTWRPMAANxOAP9x
jIGFSrKovegMzoEM+pA/Kh/wusVSX3rnOmDzhLcrmwD/V9qnMGknYRbVzqxUsb/2
hA2ru2O6spWLcWp2Bzp2PAsWIQQBZyu2fktAR+Wk7ApzHOoJLEZfyAAAff0A/3La
Uo9qQOvZ0K+X/0A5jZRRWQ9T9oz6snuqq+g1jWMkAQCZreJP5q9dG/OL79Idu1f+
TyQ13UUKZrWk4ckTQ5CvDsYzBF4L4QAWCSsGAQQB2kcPAQEHQJ0voRfJUU40amfU
pjCzrgFTBklM08Brbpc23Lz4XLz0wsALBB8WCgB9BYJeC+EAAwsJBwkQeLxTl0cL
p/BHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnafFLHWhT
Fu1UjWpdUc02tcoy7K1fMskS5415q4odmJADFQoIApsBAh4BFiEEIShzu5xMxJ+O
Wm/qeLxTl0cLp/AAAPbIAQD480LIE92EH5vh79qSLbAWbqTDTrIW1UasKSwGdsMc
OAEAhhMKRSnJx3i6MYBnhIdnGZlvOrYYW+YiIBSm6f6lDQLNEzxjYXJvbEBleGFt
cGxlLm9yZz7CwA4EExYKAIAFgl4L4QADCwkHCRB4vFOXRwun8EcUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdj3ZuCg1W6HM4EaLL7Xju3dM3/
P9zwJCQfiXGBvFDn0AMVCggCmQECmwECHgEWIQQhKHO7nEzEn45ab+p4vFOXRwun
8AAA1kIA/ipADvmNJ7oCSOVCTX3kiA/fsyJ6+duWev12PkEBsdx7AP91zFfhfoB/
08OZKP4n/fIfTBsAudtz3zifef1TvFpKBMLABwQQFgoAeQWCXoPZgAWDCWeaOwOF
AVoJEIYb9C/0kMWBRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw
Lm9yZ/KOl8au2MM+GsyMboZvL1SlBy6lTGsxuR9R46GNeQvMFiEE6kead80HRFjq
/la0hhv0L/SQxYEAAOsQAQCGmslokfigitOj9Nt+RGqFFmkrwM3tINfaRs+HRZFi
WQD8DOCaP21B8PeJSEMsFUOLRJT2T86hZuUfxR2GRC1NPw7CwAcEEBYKAHkFgl40
v4AFgwlnmjsDhQE8CRCGG/Qv9JDFgUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
ZXF1b2lhLXBncC5vcmc70NxIzQeWW6U26mZ5PWufIaoMzwdykvH4jRk7GrGw4BYh
BOpHmnfNB0RY6v5WtIYb9C/0kMWBAACsIQEA+UG5MGKpLifyT046Ug9g8ucb0E9A
LJ9NHf1D0ZV4EC0BAN6bTWuTxm1hVDsaz/U5tBWA0z+Pr/C0j8kCJG7uNNgGzjME
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
a5upBw==
=XPdm
-----END PGP PUBLIC KEY BLOCK-----

4528
tests/sq-wot.rs Normal file

File diff suppressed because it is too large Load Diff