Add a framework to format and test examples.

- The `--help` output for most subcommands includes one or more
    examples.

  - We should test these, like we test everything else.

  - Add a framework to format, and test the examples.

  - Fixes #190.

  - Also, fix some broken examples.
This commit is contained in:
Neal H. Walfield 2024-02-08 14:43:26 +01:00
parent 825f4463de
commit 0d1da78356
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
18 changed files with 681 additions and 86 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
.dir-locals.el
/*.html
/*.pdf
writelock

37
Cargo.lock generated
View File

@ -803,6 +803,17 @@ dependencies = [
"subtle",
]
[[package]]
name = "dircpy"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8466f8d28ca6da4c9dfbbef6ad4bff6f2fdd5e412d821025b0d3f0a9d74a8c1e"
dependencies = [
"jwalk",
"log",
"walkdir",
]
[[package]]
name = "dirs"
version = "5.0.1"
@ -1783,6 +1794,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56"
dependencies = [
"crossbeam",
"rayon",
]
[[package]]
name = "lalrpop"
version = "0.20.0"
@ -2755,7 +2776,7 @@ dependencies = [
"anyhow",
"serde",
"serde_yaml 0.8.26",
"textwrap",
"textwrap 0.15.2",
"thiserror",
]
@ -3081,6 +3102,7 @@ dependencies = [
"chrono",
"clap",
"clap_complete",
"dircpy",
"dirs",
"dot-writer",
"fehler",
@ -3104,7 +3126,7 @@ dependencies = [
"tempfile",
"termcolor",
"terminal_size",
"textwrap",
"textwrap 0.16.0",
"tokio",
]
@ -3575,6 +3597,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.56"

View File

@ -63,10 +63,12 @@ serde = { version = "1.0.137", features = ["derive"] }
sequoia-openpgp = { version = "1.17", default-features = false }
sequoia-net = { version = "0.28", default-features = false }
subplot-build = { version = ">=0.7, <0.10", optional = true }
textwrap = "0.16"
cfg-if = "1"
terminal_size = ">=0.2.6, <0.4"
[dev-dependencies]
dircpy = "0.3"
subplotlib = ">=0.7, <0.10"
fehler = "1.0.0"
assert_cmd = "2"

View File

@ -3,6 +3,70 @@ use clap::Parser;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use crate::cli::examples;
use examples::Action;
use examples::Actions;
use examples::Example;
const EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "Exports all certificates.",
command: &[
"sq", "cert", "export",
],
}),
Action::Example(Example {
comment: "\
Exports certificates with a matching User ID packet. The binding \
signatures are checked, but the User IDs are not authenticated. \
Note: this check is case sensitive.",
command: &[
"sq", "cert", "export",
"--userid", "Alice <alice@example.org>",
],
}),
Action::Example(Example {
comment: "\
Exports certificates with a User ID containing the email address. \
The binding signatures are checked, but the User IDs are not \
authenticated. Note: this check is case insensitive.",
command: &[
"sq", "cert", "export", "--email", "alice@example.org",
],
}),
Action::Example(Example {
comment: "\
Exports certificates where the certificate (i.e., the primary key) \
has the specified Key ID.",
command: &[
"sq", "cert", "export", "--cert", "6F0073F60FD0CBF0",
],
}),
Action::Example(Example {
comment: "\
Exports certificates where the primary key or a subkey matches the \
specified Key ID.",
command: &[
"sq", "cert", "export", "--key", "24F3955B0B8DECC8",
],
}),
Action::Example(Example {
comment: "\
Exports certificates that contain a User ID with *either* (not both!) \
email address. Note: this check is case insensitive.",
command: &[
"sq", "cert", "export",
"--email", "alice@example.org",
"--email", "bob@example.org",
],
}),
],
};
test_examples!(sq_cert_export, EXAMPLES);
#[derive(Parser, Debug)]
#[clap(
name = "export",
@ -25,34 +89,7 @@ all certificates.
Fails if search criteria are specified and none of them matches any
certificates. Note: this means if the certificate store is empty and
no search criteria are specified, then this will return success.",
after_help =
"EXAMPLES:
# Exports all certificates.
$ sq cert export > all.pgp
# Exports certificates with a matching User ID packet. The binding
# signatures are checked, but the User IDs are not authenticated.
# Note: this check is case sensitive.
$ sq cert export --userid 'Alice <alice@example.org>'
# Exports certificates with a User ID containing the email address.
# The binding signatures are checked, but the User IDs are not
# authenticated. Note: this check is case insensitive.
$ sq cert export --email 'alice@example.org'
# Exports certificates where the certificate (i.e., the primary key)
# has the specified Key ID.
$ sq cert export --cert 1234567812345678
# Exports certificates where the primary key or a subkey matches the
# specified Key ID.
$ sq cert export --key 1234567812345678
# Exports certificates that contain a User ID with *either* (not
# both!) email address. Note: this check is case insensitive.
$ sq cert export --email alice@example.org --email bob@example.org
",
after_help = EXAMPLES,
)]
pub struct Command {
#[clap(

View File

@ -2,6 +2,24 @@ use std::path::PathBuf;
use clap::Parser;
use crate::cli::examples;
use examples::Action;
use examples::Actions;
use examples::Example;
const EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "Imports a certificate.",
command: &[
"sq", "cert", "import", "juliet.pgp",
],
}),
]
};
test_examples!(sq_cert_import, EXAMPLES);
#[derive(Parser, Debug)]
#[clap(
name = "import",
@ -9,12 +27,7 @@ use clap::Parser;
long_about =
"Imports certificates into the local certificate store
",
after_help =
"EXAMPLES:
# Imports a certificate.
$ sq cert import < juliet.pgp
",
after_help = EXAMPLES,
)]
pub struct Command {
#[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")]

176
src/cli/examples.rs Normal file
View File

@ -0,0 +1,176 @@
//! A framework to format and check examples.
//!
//! The help text for subcommands includes examples. That's great.
//! But, it is even better when they are tested. This module defines
//! data structures to describe the examples, mechanisms to format the
//! examples, and infrastructure to execute the examples.
use clap::builder::IntoResettable;
use clap::builder::Resettable;
/// A command that is executed by the integration test, but not shown
/// in the manual pages.
pub struct Setup<'a> {
pub command: &'a [ &'a str ],
}
/// A command that is executed by the integration test, and shown in
/// the manual pages.
pub struct Example<'a> {
// A human-readable comment.
pub comment: &'a str,
pub command: &'a [ &'a str ],
}
/// An action to execute.
#[allow(dead_code)]
pub enum Action<'a> {
/// A command that is executed by the integration test, but not
/// shown in the manual pages.
Setup(Setup<'a>),
/// A command that is executed by the integration test, and shown
/// in the manual pages.
Example(Example<'a>),
}
impl<'a> Action<'a> {
/// Return the action's command, if any.
#[allow(dead_code)]
pub fn command(&self) -> Option<&'a [ &'a str ]> {
match self {
Action::Setup(Setup { command, .. }) => Some(command),
Action::Example(Example { command, .. }) => Some(command),
}
}
}
/// A sequence of actions to execute.
pub struct Actions<'a> {
pub actions: &'a [Action<'a>],
}
impl<'a> IntoResettable<clap::builder::StyledStr> for Actions<'a> {
fn into_resettable(self) -> Resettable<clap::builder::StyledStr> {
// Default width when we aren't connected to a terminal.
let default_width = 72;
let terminal_size = terminal_size::terminal_size();
let width = if let Some((width, _height)) = terminal_size {
let width = width.0 as usize;
if width < 40 {
// If the terminal is too narrow, then give up and use
// the default.
default_width
} else {
std::cmp::max(40, width)
}
} else {
72
};
let mut lines = vec![ "EXAMPLES:".to_string() ];
lines.extend(self.actions
.iter()
.filter_map(|action| {
let example = if let Action::Example(example) = action {
example
} else {
return None;
};
let comment = textwrap::indent(
&textwrap::wrap(example.comment, width).join("\n"),
"# ");
let command = example.command.iter()
.fold(vec!["$".to_string()], |mut s, arg| {
// Quote the argument, if necessary.
let arg = if arg.contains(&[
'\"',
]) {
format!("'{}'", arg)
} else if arg.chars().any(char::is_whitespace)
|| arg.contains(&[
'`', '#', '$', '&', '*', '(', ')',
'\\', '|', '[', ']', '{', '}',
';', '\'', '<', '>', '?', '!',
])
{
format!("\"{}\"", arg)
} else {
arg.to_string()
};
let last = s.last_mut().expect("have one");
let last_chars = last.chars().count();
let arg_chars = arg.chars().count();
// Our manpage generate complains if an
// example is too long:
//
// warning: Command in example exceeds 64 chars:
if last_chars + 1 + arg_chars <= width.min(64) {
*last = format!("{} {}", last, arg);
} else {
*last = format!("{} \\", last);
s.push(format!(" {}", arg));
}
s
})
.join("\n");
Some(format!("{}\n{}", comment, command))
}));
let text = lines.join("\n\n").into();
Resettable::Value(text)
}
}
macro_rules! test_examples {
($ident:ident, $actions:expr) => {
#[test]
fn $ident() {
use std::path::PathBuf;
use tempfile::TempDir;
use assert_cmd::Command;
let fixtures = PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/examples"));
let tmp_dir = TempDir::new().unwrap();
dircpy::copy_dir(&fixtures, &tmp_dir)
.expect(&format!("Copying {:?} to {:?}",
fixtures, &tmp_dir));
let cert_store = tmp_dir.path().join("cert-store");
for action in $actions.actions {
let command = if let Some(command) = action.command() {
command
} else {
continue;
};
eprintln!("Executing: {:?}", command);
Command::cargo_bin(command[0]).unwrap()
.current_dir(&tmp_dir)
.env("SQ_CERT_STORE", &cert_store)
.args(&command[1..])
.assert()
.success();
}
}
};
}

View File

@ -86,6 +86,9 @@ pub mod autocrypt;
use sequoia_openpgp as openpgp;
use openpgp::Fingerprint;
#[macro_use]
pub mod examples;
pub mod cert;
pub mod decrypt;
pub mod encrypt;

View File

@ -14,6 +14,12 @@ use crate::cli::types::TrustAmount;
pub mod certify;
pub mod link;
use crate::cli::examples;
use examples::Action;
use examples::Setup;
use examples::Actions;
use examples::Example;
#[derive(Debug, Parser)]
#[clap(
name = "pki",
@ -54,6 +60,38 @@ pub enum Subcommands {
Path(PathCommand),
}
const AUTHENTICATE_EXAMPLES: Actions = Actions {
actions: &[
// Link Alice's certificate.
Action::Setup(Setup {
command: &[
"sq", "pki", "link", "add",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0", "--all",
],
}),
Action::Example(Example {
comment: "\
Authenticate a specific binding.",
command: &[
"sq", "pki", "authenticate",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
"Alice <alice@example.org>",
]
}),
Action::Example(Example {
comment: "\
Check whether we can authenticate any user ID with the specified email \
address for the given certificate.",
command: &[
"sq", "pki", "authenticate",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
"--email", "alice@example.org",
],
}),
]
};
test_examples!(sq_pki_authenticate, AUTHENTICATE_EXAMPLES);
/// Authenticate a binding.
///
/// Authenticate a binding (a certificate and User ID) by looking
@ -72,20 +110,8 @@ pub enum Subcommands {
#[derive(Parser, Debug)]
#[clap(
name = "authenticate",
after_help =
"EXAMPLES:
# Authenticate a binding.
$ sq pki authenticate --partial \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
'Alice <alice@example.org>'
# Try and authenticate each binding where the User ID has the
# specified email address.
$ sq pki authenticate \\
C7966E3E7CE67DBBECE5FC154E2AD944CFC78C86 \\
--email alice@example.org
")]
after_help = AUTHENTICATE_EXAMPLES,
)]
pub struct AuthenticateCommand {
#[command(flatten)]
pub email: EmailArg,
@ -106,6 +132,34 @@ pub struct AuthenticateCommand {
pub userid: UserIDArg,
}
const LOOKUP_EXAMPLES: Actions = Actions {
actions: &[
// Link Alice's certificate.
Action::Setup(Setup {
command: &[
"sq", "pki", "link", "add",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0", "--all",
],
}),
Action::Example(Example {
comment: "\
Lookup certificates that can be authenticated for the given user ID.",
command: &[
"sq", "pki", "lookup", "Alice <alice@example.org>"
],
}),
Action::Example(Example {
comment: "\
Lookup certificates that have a user ID with the specified email \
address, and that user ID can be authenticated.",
command: &[
"sq", "pki", "lookup", "--email", "alice@example.org",
],
}),
]
};
test_examples!(sq_pki_lookup, LOOKUP_EXAMPLES);
/// Lookup the certificates associated with a User ID.
///
/// Identifies authenticated bindings (User ID and certificate
@ -121,15 +175,8 @@ pub struct AuthenticateCommand {
#[derive(Parser, Debug)]
#[clap(
name = "lookup",
after_help =
"EXAMPLES:
# Lookup a certificate with the given User ID.
$ sq pki lookup --partial 'Alice <alice@example.org>'
# Lookup a certificate with the given email address.
$ sq pki lookup --email alice@example.org
")]
after_help = LOOKUP_EXAMPLES,
)]
pub struct LookupCommand {
#[command(flatten)]
pub email: EmailArg,
@ -147,6 +194,35 @@ pub struct LookupCommand {
pub userid: UserIDArg,
}
const IDENTIFY_EXAMPLES: Actions = Actions {
actions: &[
// Link Alice's certificate.
Action::Setup(Setup {
command: &[
"sq", "pki", "link", "add",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0", "--all",
],
}),
Action::Example(Example {
comment: "\
Identify the user IDs that can be authenticated for the certificate.",
command: &[
"sq", "pki", "identify",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
],
}),
Action::Example(Example {
comment: "\
List all user IDs that have that have been certified by anyone.",
command: &[
"sq", "pki", "identify", "--gossip",
"511257EBBF077B7AEDAE5D093F68CB84CE537C9A",
],
}),
]
};
test_examples!(sq_pki_identify, IDENTIFY_EXAMPLES);
/// Identify a certificate.
///
/// Identify a certificate by finding authenticated bindings (User
@ -162,17 +238,8 @@ pub struct LookupCommand {
#[derive(Parser, Debug)]
#[clap(
name = "identify",
after_help =
"EXAMPLES:
# Identify a certificate.
$ sq pki identify --partial \\
C7B1406CD2F612E9CE2136156F2DA183236153AE
# Get gossip about a certificate.
$ sq pki identify --gossip \\
3217C509292FC67076ECD75C7614269BDDF73B36
")]
after_help = IDENTIFY_EXAMPLES,
)]
pub struct IdentifyCommand {
#[command(flatten)]
pub gossip: GossipArg,
@ -187,6 +254,26 @@ pub struct IdentifyCommand {
pub cert: CertArg,
}
const LIST_EXAMPLES: Actions = Actions {
actions: &[
Action::Setup(Setup {
command: &[
"sq", "pki", "link", "add",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0", "--all",
],
}),
Action::Example(Example {
comment: "\
List all bindings for user IDs containing an email address from \
example.org, and that can be authenticated.",
command: &[
"sq", "pki", "list", "@example.org",
],
})
]
};
test_examples!(sq_pki_list, LIST_EXAMPLES);
/// List all authenticated bindings (User ID and certificate
/// pairs).
///
@ -204,13 +291,8 @@ pub struct IdentifyCommand {
#[derive(Parser, Debug)]
#[clap(
name = "list",
after_help =
"EXAMPLES:
# List all bindings for example.org that are at least partially
# authenticated.
$ sq pki list --partial @example.org
")]
after_help = LIST_EXAMPLES,
)]
pub struct ListCommand {
#[command(flatten)]
pub email: EmailArg,
@ -233,6 +315,22 @@ pub struct ListCommand {
pub pattern: Option<String>,
}
const PATH_EXAMPLES: Actions = Actions {
actions: &[
Action::Example(Example {
comment: "\
Verify that Alice ceritified a particular User ID for Bob's certificate.",
command: &[
"sq", "pki", "path",
"EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
"511257EBBF077B7AEDAE5D093F68CB84CE537C9A",
"Bob <bob@example.org>",
],
})
],
};
test_examples!(sq_pki_path, PATH_EXAMPLES);
/// Verify the specified path.
///
/// A path is a sequence of certificates starting at the root, and
@ -248,15 +346,8 @@ pub struct ListCommand {
#[derive(Parser, Debug)]
#[clap(
name = "path",
after_help =
"EXAMPLES:
# Verify that Neal ceritified Justus's certificate for a particular User ID.
$ sq pki path \\
8F17777118A33DDA9BA48E62AACB3243630052D9 \\
CBCD8F030588653EEDD7E2659B7DD433F254904A \\
'Justus Winter <justus@sequoia-pgp.org>'
")]
after_help = PATH_EXAMPLES,
)]
pub struct PathCommand {
#[command(flatten)]
pub gossip: GossipArg,

View File

@ -0,0 +1,29 @@
# Introduction
This directory contains data for the examples.
The test suite executes each subcommand's examples. Each subcommand
has its own context (temporary directory), which is set to the current
working directory. The contents of this directory are copied into
that directory. If a subcommand has multiple examples, they are
execute after each other in the same context.
By using static data, we can use known fingerprints in the examples.
# Contents
- alice-secret.pgp: A general-purpose certificate for Alice
<alice@example.org>.
- Imported into the cert store.
- bob-secret.pgp: A general-purpose certificate for Bob
<bob@example.org>.
- Imported into the cert store.
- Certified by Alice.
- juliet.pgp: A general-purpose certificate for Juliet Capulet
<juliet@example.org>.
- NOT imported into the cert store.

View File

@ -0,0 +1,43 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: EB28 F26E 2739 A487 0ECC 4772 6F00 73F6 0FD0 CBF0
Comment: Alice <alice@example.org>
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
=3xfI
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Revocation certificate for
Comment: EB28 F26E 2739 A487 0ECC 4772 6F00 73F6 0FD0 CBF0
Comment: Alice <alice@example.org>
xjMEZcT3URYJKwYBBAHaRw8BAQdASgo8UO9lxwMTZkkBrqzyUMjF4flY8YtRT+pk
iSI2jlPCwAoEIBYKAH0FgmXE91EJEG8Ac/YP0MvwRxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZ1F95aiZHzvfQFFbhbE0SsQy3vMeM+nfRves
qRkHca4nDR0AVW5zcGVjaWZpZWQWIQTrKPJuJzmkhw7MR3JvAHP2D9DL8AAALnYA
/24KMLbwtB17x6wo3b1dUu1WODBxQCJj28+hC+IBMhIaAPjYZbEum4myHuOPg/2v
Y5dSMQaYBpYYcYRu7g5UfugE
=+8np
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,43 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: 5112 57EB BF07 7B7A EDAE 5D09 3F68 CB84 CE53 7C9A
Comment: Bob <bob@example.org>
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
=qucl
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Revocation certificate for
Comment: 5112 57EB BF07 7B7A EDAE 5D09 3F68 CB84 CE53 7C9A
Comment: Bob <bob@example.org>
xjMEZcT3SRYJKwYBBAHaRw8BAQdAlN1milRLfSk/XwgEuKeZ27kK1sZcfRroOqjZ
+E+OJFLCwAsEIBYKAH0FgmXE90kJED9oy4TOU3yaRxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZ4Bk7ktrmF8plMFxGXxG/UF8Ne49zSqaitah
szsKnEQcDR0AVW5zcGVjaWZpZWQWIQRRElfrvwd7eu2uXQk/aMuEzlN8mgAAmB0A
/0F03E1UJ8/iWMtc0U8a2KP0nKHLXQvdzZFTXYGQs+zBAQC/wldPhJ51fJucKrOh
QpVC2oxC1IkcBewTXG9yhl4oCg==
=iw4M
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,44 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Comment: 7A58 B15E 3B94 5948 3D9F FA8D 40E2 99AC 5F2B 0872
Comment: Juliet Capulet <juliet@example.org>
xVgEZcT8ehYJKwYBBAHaRw8BAQdA3lxROHivYEc3bXTGDtoMuhY3rPN9zitmSEsQ
3uMawTcAAQDdqSYdugT8ZYiXwmQupew5ioqT2q+K72pViJjuwLg4tRIBwsALBB8W
CgB9BYJlxPx6AwsJBwkQQOKZrF8rCHJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu
c2VxdW9pYS1wZ3Aub3JnjVASiFqRzmkGe1LPiY4olToBnIdQ8Xx3b4kTwjJPj7kD
FQoIApsBAh4BFiEEelixXjuUWUg9n/qNQOKZrF8rCHIAAP4TAPsG6HdFuXr9LEUk
38Nms2/d38hoCdPfwsPnkZYg1bOK1QEAkJah86jp9JVSyAfBO5csKdNEf6R1dXY7
9xu1Sw67xwjNI0p1bGlldCBDYXB1bGV0IDxqdWxpZXRAZXhhbXBsZS5vcmc+wsAO
BBMWCgCABYJlxPx6AwsJBwkQQOKZrF8rCHJHFAAAAAAAHgAgc2FsdEBub3RhdGlv
bnMuc2VxdW9pYS1wZ3Aub3Jn1FqFnmGhxznlWBLFoRa61r8XigiWHC5KrzWmtJ59
u0oDFQoIApkBApsBAh4BFiEEelixXjuUWUg9n/qNQOKZrF8rCHIAACMeAQDZ/w4T
bbmIkxk/BCaRMbnayahiHhfF0c3JjyTXgFdwpQEAvpr686uEUv6IAfLZ6hQHnYdm
xbE+6lfSsEZDF/wiUQLHWARlxPx6FgkrBgEEAdpHDwEBB0A8syceHf01lFn79GP4
MnatpXkNXtQYeH4FoDmzJ06oygABAKOJCYTCFGd/FuhSYS7Ft/9vvg5w+DBZotGq
mJEY+afZENPCwL8EGBYKATEFgmXE/HoJEEDimaxfKwhyRxQAAAAAAB4AIHNhbHRA
bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ4sTm8Sq4aSpl0VRIrZiftDohY9IikmW
S6oMj2oFC1BuApsCvqAEGRYKAG8FgmXE/HoJEIMEhL2szEMlRxQAAAAAAB4AIHNh
bHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyjznJGJYDmjWouq93Q9UX+2FV6d
siqY+4pueKLbnY4aFiEEzb/YfSE60fznV0XtgwSEvazMQyUAAG2+AP4u3zNN3FbO
dh/s0qEomEKIxJjRzyh2IzSomjTpvubGSQD/clAoUe02I7OpI4va44Zpw05ggHDz
fhdlJyEVeCY0+gQWIQR6WLFeO5RZSD2f+o1A4pmsXysIcgAAfXQA/jJw9jurz03j
ZPRY6iHsKYCWsh9WHxUFLD1I4ToS2sqjAP49kjsXhsdp9TrIzmfmQc8hI03Nnmb7
MmrGNu1QkUbrAsdYBGXE/HoWCSsGAQQB2kcPAQEHQP3mXvoWQpYwbd6xWnL8HpfU
7gNra50ZxQ2h4pskKi3SAAEA0cWm7zsZgoNxyiSdK8oLZOQB0+bpYZXSkv5/ra+f
nxgR9cLAvwQYFgoBMQWCZcT8egkQQOKZrF8rCHJHFAAAAAAAHgAgc2FsdEBub3Rh
dGlvbnMuc2VxdW9pYS1wZ3Aub3Jnc0DfMNymRnpmuM1MysdH+VZ/X91dnrPjRqtR
n1qM1tACmyC+oAQZFgoAbwWCZcT8egkQdruOXnrPkTxHFAAAAAAAHgAgc2FsdEBu
b3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JngrW/Tm58TweTDzESH30JAXNgDlm109eV
FdZj+Cn0YfQWIQTf1CEOTUC++4f87+l2u45ees+RPAAAzcsBAPwPxVqIG1KvJ0ds
h8RoYYUCef+R3Ax5vSsnObb3FXscAQDSgesDtTZMw+9NLA0jhK+AUxyqzBdaMlbx
McG6gWuGDRYhBHpYsV47lFlIPZ/6jUDimaxfKwhyAAAr0wD/UVJZoEQNGYRfZ4X1
1thogaYcem4o/ek1PxWUqn44eBQBAJguCPgRAU8xc1WaTTWXAKU12+jg/tfcEkBk
dfP/Ru8Ox10EZcT8ehIKKwYBBAGXVQEFAQEHQLljeYVLH9L60OcEfd1zhUpRoWKf
g2EoDZCtRTEcxLhWAwEIBwAA/2xY5dNDiFdUOu1ankI4u9sL6c0BSPU3HrenQtHO
3hOoEUzCwAAEGBYKAHIFgmXE/HoJEEDimaxfKwhyRxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZ5w4sR1zgA6dC58p7zNfpE3vdqfDkCl3qbbr
UquoseL9ApsMFiEEelixXjuUWUg9n/qNQOKZrF8rCHIAAMBRAP4rF1TtU5MWY13d
LDJIsrmrVhaVFO8eGkk11bu4DsGjsAD/TAeeh3DTER/zyUssCRFd2ihvLSWk+laL
PCdvFL+YTwY=
=iIGf
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,41 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: 7A58 B15E 3B94 5948 3D9F FA8D 40E2 99AC 5F2B 0872
Comment: Juliet Capulet <juliet@example.org>
xjMEZcT8ehYJKwYBBAHaRw8BAQdA3lxROHivYEc3bXTGDtoMuhY3rPN9zitmSEsQ
3uMawTfCwAsEHxYKAH0FgmXE/HoDCwkHCRBA4pmsXysIckcUAAAAAAAeACBzYWx0
QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeNUBKIWpHOaQZ7Us+JjiiVOgGch1Dx
fHdviRPCMk+PuQMVCggCmwECHgEWIQR6WLFeO5RZSD2f+o1A4pmsXysIcgAA/hMA
+wbod0W5ev0sRSTfw2azb93fyGgJ09/Cw+eRliDVs4rVAQCQlqHzqOn0lVLIB8E7
lywp00R/pHV1djv3G7VLDrvHCM0jSnVsaWV0IENhcHVsZXQgPGp1bGlldEBleGFt
cGxlLm9yZz7CwA4EExYKAIAFgmXE/HoDCwkHCRBA4pmsXysIckcUAAAAAAAeACBz
YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfUWoWeYaHHOeVYEsWhFrrWvxeK
CJYcLkqvNaa0nn27SgMVCggCmQECmwECHgEWIQR6WLFeO5RZSD2f+o1A4pmsXysI
cgAAIx4BANn/DhNtuYiTGT8EJpExudrJqGIeF8XRzcmPJNeAV3ClAQC+mvrzq4RS
/ogB8tnqFAedh2bFsT7qV9KwRkMX/CJRAs4zBGXE/HoWCSsGAQQB2kcPAQEHQDyz
Jx4d/TWUWfv0Y/gydq2leQ1e1Bh4fgWgObMnTqjKwsC/BBgWCgExBYJlxPx6CRBA
4pmsXysIckcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeL
E5vEquGkqZdFUSK2Yn7Q6IWPSIpJlkuqDI9qBQtQbgKbAr6gBBkWCgBvBYJlxPx6
CRCDBIS9rMxDJUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v
cmco85yRiWA5o1qLqvd0PVF/thVenbIqmPuKbnii252OGhYhBM2/2H0hOtH851dF
7YMEhL2szEMlAABtvgD+Lt8zTdxWznYf7NKhKJhCiMSY0c8odiM0qJo06b7mxkkA
/3JQKFHtNiOzqSOL2uOGacNOYIBw834XZSchFXgmNPoEFiEEelixXjuUWUg9n/qN
QOKZrF8rCHIAAH10AP4ycPY7q89N42T0WOoh7CmAlrIfVh8VBSw9SOE6EtrKowD+
PZI7F4bHafU6yM5n5kHPISNNzZ5m+zJqxjbtUJFG6wLOMwRlxPx6FgkrBgEEAdpH
DwEBB0D95l76FkKWMG3esVpy/B6X1O4Da2udGcUNoeKbJCot0sLAvwQYFgoBMQWC
ZcT8egkQQOKZrF8rCHJHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w
Z3Aub3Jnc0DfMNymRnpmuM1MysdH+VZ/X91dnrPjRqtRn1qM1tACmyC+oAQZFgoA
bwWCZcT8egkQdruOXnrPkTxHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p
YS1wZ3Aub3JngrW/Tm58TweTDzESH30JAXNgDlm109eVFdZj+Cn0YfQWIQTf1CEO
TUC++4f87+l2u45ees+RPAAAzcsBAPwPxVqIG1KvJ0dsh8RoYYUCef+R3Ax5vSsn
Obb3FXscAQDSgesDtTZMw+9NLA0jhK+AUxyqzBdaMlbxMcG6gWuGDRYhBHpYsV47
lFlIPZ/6jUDimaxfKwhyAAAr0wD/UVJZoEQNGYRfZ4X11thogaYcem4o/ek1PxWU
qn44eBQBAJguCPgRAU8xc1WaTTWXAKU12+jg/tfcEkBkdfP/Ru8OzjgEZcT8ehIK
KwYBBAGXVQEFAQEHQLljeYVLH9L60OcEfd1zhUpRoWKfg2EoDZCtRTEcxLhWAwEI
B8LAAAQYFgoAcgWCZcT8egkQQOKZrF8rCHJHFAAAAAAAHgAgc2FsdEBub3RhdGlv
bnMuc2VxdW9pYS1wZ3Aub3JnnDixHXOADp0LnynvM1+kTe92p8OQKXeptutSq6ix
4v0CmwwWIQR6WLFeO5RZSD2f+o1A4pmsXysIcgAAwFEA/isXVO1TkxZjXd0sMkiy
uatWFpUU7x4aSTXVu7gOwaOwAP9MB56HcNMRH/PJSywJEV3aKG8tJaT6Vos8J28U
v5hPBg==
=K2JW
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Revocation certificate for
Comment: 7A58 B15E 3B94 5948 3D9F FA8D 40E2 99AC 5F2B 0872
Comment: Juliet Capulet <juliet@example.org>
xjMEZcT8ehYJKwYBBAHaRw8BAQdA3lxROHivYEc3bXTGDtoMuhY3rPN9zitmSEsQ
3uMawTfCwAsEIBYKAH0FgmXE/HoJEEDimaxfKwhyRxQAAAAAAB4AIHNhbHRAbm90
YXRpb25zLnNlcXVvaWEtcGdwLm9yZxxCMSrMAAjxv4PmtEkn5JBHwnxAVs3ncsRF
3/T+EHBpDR0AVW5zcGVjaWZpZWQWIQR6WLFeO5RZSD2f+o1A4pmsXysIcgAAj+0B
AK7rgH7a6EVFGJaKHEQ2bs4fBlZdb8kPH+8/88k74DExAQDIUvW6MgRaGttYe/jG
W6J9FAXu+9cIkroYwCREOJYaCg==
=TEVF
-----END PGP PUBLIC KEY BLOCK-----