use std::borrow::Cow;
use std::io;
use std::sync::Mutex;
use git2::{Oid, Repository};
use serde::Serialize;
use openpgp::{
Cert,
Fingerprint,
Packet,
packet::Signature,
};
use super::*;
#[derive(Clone)]
pub enum Format {
HumanReadable,
Json,
}
impl std::str::FromStr for Format {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"human-readable" => Ok(Self::HumanReadable),
"json" => Ok(Self::Json),
_ => Err(anyhow!("unknown output format {:?}", s)),
}
}
}
pub fn describe_policy(p: &Policy) -> Result<()> {
println!("# OpenPGP policy file for git, version {}",
p.version());
println!();
println!("## Commit Goodlist");
println!();
for commit in p.commit_goodlist() {
println!(" - {}", commit);
}
println!();
println!("## Authorizations");
println!();
for (i, (name, auth)) in p.authorization().iter().enumerate()
{
println!("{}. {}", i, name);
let ident = vec![' '; i.to_string().len() + 2]
.into_iter().collect::<String>();
if auth.sign_commit {
println!("{}- may sign commits", ident);
}
if auth.sign_tag {
println!("{}- may sign tags", ident);
}
if auth.sign_archive {
println!("{}- may sign archives", ident);
}
if auth.add_user {
println!("{}- may add users", ident);
}
if auth.retire_user {
println!("{}- may retire users", ident);
}
if auth.audit {
println!("{}- may goodlist commits", ident);
}
for cert in auth.certs()? {
println!("{}- has OpenPGP cert: {}", ident,
cert?.fingerprint());
}
}
Ok(())
}
pub fn describe_diff(p: &Diff) -> Result<()> {
fn quote<'a>(s: &'a str) -> Cow<'a, str> {
if s.chars().any(|c| {
! (c.is_alphanumeric()
|| ['-', '_', '.', '+'].contains(&c))
})
{
format!("{:?}", s).into()
} else {
s.into()
}
}
let quote_component = |c: &Packet| -> String {
match c {
Packet::PublicSubkey(k) => {
format!("subkey {}", k.fingerprint())
}
Packet::SecretSubkey(k) => {
format!("subkey (with secret key material) {}", k.fingerprint())
}
Packet::UserID(u) => {
format!("user ID {}", quote(&String::from_utf8_lossy(u.value())))
}
c => {
format!("{} ({:?})", c.tag(), c)
}
}
};
let is_self_signed = |cert: &Fingerprint, sig: &Signature| -> bool {
let cert = KeyHandle::from(cert);
sig.get_issuers().into_iter().any(|kh| kh.aliases(&cert))
};
for change in &p.changes {
use Change::*;
match change {
VersionChange { from, to } =>
println!(" - Version changed from {} to {}.", from, to),
GoodlistCommit(oid) =>
println!(" - Commit {} was added to the goodlist.", oid),
UngoodlistCommit(oid) =>
println!(" - Commit {} was removed from the goodlist.", oid),
AddUser(name) =>
println!(" - User {} was added.", quote(name)),
RetireUser(name) =>
println!(" - User {} was retired.", quote(name)),
AddRight(name, right) =>
println!(" - User {} was granted the right {}.",
quote(name), right),
RemoveRight(name, right) =>
println!(" - User {}'s {} right was revoked.",
quote(name), right),
AddCert(name, fpr) =>
println!(" - User {}: new certificate {}.", quote(name), fpr),
RemoveCert(name, fpr) =>
println!(" - User {}: removed certificate {}.", quote(name), fpr),
AddPacket(name, fpr, component, sig) =>
println!(" - User {}'s certificate {} has a new {} signature {:02x}{:02x} on {}.",
quote(name), fpr,
if is_self_signed(fpr, sig) {
"self-signed"
} else {
"third-party"
},
sig.digest_prefix()[0],
sig.digest_prefix()[1],
quote_component(component)),
RemovePacket(name, fpr, component, sig) =>
println!(" - User {}'s certificate {} lost the {} signature {:02x}{:02x} on {}.",
quote(name), fpr,
if is_self_signed(fpr, sig) {
"self-signed"
} else {
"third-party"
},
sig.digest_prefix()[0],
sig.digest_prefix()[1],
quote_component(component)),
}
}
Ok(())
}
static COMMIT_JSON_VERSION: &'static str = "1.0.0";
#[derive(Serialize)]
pub struct Commit<'a> {
version: &'static str,
#[serde(serialize_with = "crate::utils::serialize_oid")]
id: &'a Oid,
summary: Option<String>,
#[serde(serialize_with = "crate::utils::serialize_optional_oid")]
parent_id: Option<&'a Oid>,
results: Vec<std::result::Result<String, (String, Option<String>)>>,
}
static MISSING_SIGNATURE_HINT: Mutex<bool> = Mutex::new(false);
static MISSING_KEY_HINT: Mutex<bool> = Mutex::new(false);
static MALFORMED_MESSAGE_HINT: Mutex<bool> = Mutex::new(false);
impl<'a> Commit<'a> {
pub fn new(git: &Repository,
id: &'a Oid,
parent_id: Option<&'a Oid>,
shadow_policy: &Option<PathBuf>,
result: &'a sequoia_git::Result<Vec<sequoia_git::Result<(String, Signature, Cert, Fingerprint)>>>)
-> Result<Self>
{
let hint = |e: &Error| -> Option<String> {
match (shadow_policy, e) {
(None, _) => None,
(Some(p), Error::MissingSignature(commit)) => {
let mut shown = MISSING_SIGNATURE_HINT.lock().unwrap();
if ! *shown {
*shown = true;
Some(format!("when using an external policy, do\n\n\
git show {1} \n\
\n and verify that the commit is good. \
If satisfied, do\n\n\
sq-git policy goodlist --policy-file {0} {1}",
p.display(), commit))
} else {
None
}
}
(Some(p), Error::MissingKey(handle)) => {
let mut shown = MISSING_KEY_HINT.lock().unwrap();
if ! *shown {
*shown = true;
Some(format!("when using an external policy, do\n\n\
sq keyserver get {1} \n\
\n and verify that the cert belongs to the \
committer. If satisfied, do\n\n\
sq-git policy authorize --policy-file {} \
<ROLE-NAME> {} --sign-commit",
p.display(), handle))
} else {
None
}
}
(_, Error::Other(e)) => {
if let Some(e) = e.downcast_ref::<openpgp::Error>() {
if let openpgp::Error::MalformedMessage(_) = e {
let mut shown
= MALFORMED_MESSAGE_HINT.lock().unwrap();
if ! *shown {
*shown = true;
Some(format!("\
a signature is malformed. It was probably created by GitHub, which\n\
is known to created invalid signatures. See the following discussion for\n\
more information:\n\
\n\
https://github.com/orgs/community/discussions/27607"))
} else {
None
}
} else {
None
}
} else {
None
}
}
_ => None,
}
};
let mut r = Vec::new();
match result {
Ok(results) => {
for e in results.iter()
.filter_map(|r| r.as_ref().err())
{
r.push(Err((e.to_string(), hint(e))));
}
for (name, _s, c, _signer_fpr) in results.iter()
.filter_map(|r| r.as_ref().ok())
{
r.push(Ok(format!("{} [{}]", name, c.fingerprint())));
}
},
Err(e) => {
r.push(Err((e.to_string(), hint(e))));
},
}
let mut summary = None;
match git.find_commit(id.clone()) {
Ok(commit) => {
summary = commit.summary().map(String::from);
}
Err(err) => {
eprintln!("Error looking up commit: {}", err);
}
}
Ok(Commit {
version: COMMIT_JSON_VERSION,
id,
summary,
parent_id,
results: r,
})
}
pub fn describe(&self, sink: &mut dyn io::Write, verbosity: &Verbosity) -> Result<()> {
for r in &self.results {
let id = if let Some(parent_id) = self.parent_id {
format!("Authenticating {} with {}",
self.id, parent_id)
} else {
self.id.to_string()
};
match r {
Err((e, hint)) => {
if ! verbosity.quiet() {
writeln!(sink, "{}:\n Error: {}", id, e)?;
if let Some(summary) = self.summary.as_ref() {
writeln!(sink, " {}", summary)?;
}
if let Some(h) = hint {
writeln!(sink, "\n Hint: {}", h)?;
}
}
},
Ok(fp) => {
if verbosity.verbose() {
writeln!(sink, "{}:\n Signer: {}", id, fp)?;
if let Some(summary) = self.summary.as_ref() {
writeln!(sink, " {}", summary)?;
}
}
},
}
}
Ok(())
}
}
static ARCHIVE_JSON_VERSION: &'static str = "1.0.0";
#[derive(Serialize)]
pub struct Archive {
version: &'static str,
results: Vec<std::result::Result<String, String>>,
}
impl Archive {
pub fn new(result: sequoia_git::Result<Vec<sequoia_git::Result<(String, Signature, Cert, Fingerprint)>>>)
-> Result<Self>
{
let mut r = Vec::new();
match result {
Ok(results) => {
for e in results.iter()
.filter_map(|r| r.as_ref().err())
{
r.push(Err(e.to_string()));
}
for (name, _s, c, _signer_fpr) in results.iter()
.filter_map(|r| r.as_ref().ok())
{
r.push(Ok(format!("{} [{}]", name, c.fingerprint())));
}
},
Err(e) => {
r.push(Err(e.to_string()));
},
}
Ok(Self {
version: ARCHIVE_JSON_VERSION,
results: r,
})
}
pub fn describe(&self, sink: &mut dyn io::Write) -> Result<()> {
for r in &self.results {
match r {
Err(e) => {
writeln!(sink, "{}", e)?;
},
Ok(fp) => {
writeln!(sink, "{}", fp)?;
},
}
}
Ok(())
}
}