Lines
52.12 %
Functions
25 %
use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone};
use review_harvest::gpg::{
check_signature, load_keys, MissingKeyError, PgpSignature, SignatureErrors,
};
use sequoia_openpgp::{Cert, KeyHandle};
use serde::Serialize;
/// A simple application that lists digital signatures from a git repository
use std::path::PathBuf;
type Result<T> = anyhow::Result<T>;
const BEGIN_PGP_SIGNATURE: &[u8] = b"-----BEGIN PGP SIGNATURE-----\n";
const END_PGP_SIGNATURE: &[u8] = b"-----END PGP SIGNATURE-----";
const BEGIN_PGP_MESSAGE: &[u8] = b"-----BEGIN PGP MESSAGE-----\n";
const END_PGP_MESSAGE: &[u8] = b"-----END PGP MESSAGE-----";
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn find_begin_end<'a>(
msg: &'a [u8],
begin: &'static [u8],
end: &'static [u8],
) -> Option<(&'a [u8], &'a [u8])> {
if msg.len() < begin.len() + end.len() + 1 {
return None;
let maybe_end = if msg[msg.len() - 1] == b'\n' {
&msg[msg.len() - end.len() - 1..msg.len() - 1]
} else {
&msg[msg.len() - end.len()..]
if maybe_end != end {
if let Some(first) = find_subsequence(msg, begin) {
let signature = &msg[first..];
return Some((&msg[..first], signature));
None
fn get_pgp_signature(msg: Option<&[u8]>) -> Option<(&[u8], &[u8])> {
let msg = msg?;
if let Some(r) = find_begin_end(msg, BEGIN_PGP_SIGNATURE, END_PGP_SIGNATURE) {
return Some(r);
if let Some(r) = find_begin_end(msg, BEGIN_PGP_MESSAGE, END_PGP_MESSAGE) {
struct PartialGitSignature {
tag: String,
oid: String,
name: Option<String>,
email: Option<String>,
datetime: Option<DateTime<FixedOffset>>,
fn strip_prefix<'a>(bytes: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
if bytes.len() >= prefix.len() && &bytes[..prefix.len()] == prefix {
return Some(&bytes[prefix.len()..]);
fn split_at_newline(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
if let Some(pos) = bytes.iter().position(|b| *b == b'\n') {
return Some((&bytes[..pos], &bytes[pos + 1..]));
fn get_line_value<'a>(bytes: &'a [u8], prefix: &[u8]) -> Option<(&'a [u8], &'a [u8])> {
let value = strip_prefix(bytes, prefix)?;
split_at_newline(value)
/// Parse a value of the form
/// 'First Last <flast@kernel.org> 1493772623 +0430'
fn parse_tagger(tagger: &[u8]) -> Option<(String, String, DateTime<FixedOffset>)> {
let gt_pos = tagger.iter().position(|b| *b == b'<')?;
if gt_pos == 0 || tagger[gt_pos - 1] != b' ' {
let name = &tagger[..gt_pos - 1];
let rest = &tagger[gt_pos + 1..];
let lt_pos = rest.iter().position(|b| *b == b'>')?;
if lt_pos + 1 == rest.len() || rest[lt_pos + 1] != b' ' {
let email = &rest[..lt_pos];
let rest = &rest[lt_pos + 2..];
let space_pos = rest.iter().position(|b| *b == b' ')?;
let seconds = &rest[..space_pos];
let tz = &rest[space_pos + 1..];
let name = String::from_utf8(name.to_vec()).ok()?;
let email = String::from_utf8(email.to_vec()).ok()?;
let unix_timestamp_seconds: i64 = std::str::from_utf8(seconds)
.ok()
.and_then(|s| s.parse::<i64>().ok())?;
if tz.len() != 5 {
let offset_hours = std::str::from_utf8(&tz[1..2])
.and_then(|s| s.parse::<i32>().ok())?;
let offset_minutes = std::str::from_utf8(&tz[3..4])
let offset_seconds = offset_hours * 3600 + offset_minutes * 60;
let dt = NaiveDateTime::from_timestamp_opt(unix_timestamp_seconds, 0)?;
let offset = if tz[0] == b'-' {
FixedOffset::west_opt(offset_seconds)
FixedOffset::east_opt(offset_seconds)
}?;
let datetime = offset.from_utc_datetime(&dt);
Some((name, email, datetime))
/// obtain name, email, date, time and tag from the text of the commit
fn parse_payload(payload: &[u8]) -> Option<PartialGitSignature> {
let (object, remainder) = get_line_value(payload, b"object ")?;
let oid = String::from_utf8(object.to_vec()).ok()?;
let (tag, remainder) = get_line_value(remainder, b"type commit\ntag ")
.or_else(|| get_line_value(remainder, b"type tag\ntag "))?;
let tag = String::from_utf8(tag.to_vec()).ok()?;
let mut name = None;
let mut email = None;
let mut datetime = None;
if let Some((tagger, _)) = get_line_value(remainder, b"tagger ") {
if let Some((n, e, dt)) = parse_tagger(tagger) {
name = Some(n);
email = Some(e);
datetime = Some(dt);
Some(PartialGitSignature {
tag,
oid,
name,
email,
datetime,
})
/// Retrieve the signature of the tag if it has one.
fn get_commit_signature(commit: &git2::Commit) -> Option<TagSignature> {
let mergetag = commit.header_field_bytes("mergetag").ok()?;
let (message, signature) = get_pgp_signature(Some(&mergetag))?;
if let Some(partial) = parse_payload(message) {
Some(TagSignature {
tag: partial.tag,
oid: partial.oid,
name: partial.name,
email: partial.email,
datetime: partial.datetime,
signature: PgpSignature {
payload: message.to_vec(),
signature: signature.to_vec(),
},
println!(
"could not parse message '{}'",
String::from_utf8_lossy(message)
);
fn get_tag_signature(tag: &git2::Tag) -> Option<TagSignature> {
let (message, signature) = get_pgp_signature(tag.message_bytes())?;
let tag_name = tag.name()?;
// create a payload for checking the digital signature
// see https://git-scm.com/docs/signature-format
let mut payload: Vec<u8> = Vec::with_capacity(256 + message.len());
payload.extend_from_slice(b"object ");
let id = format!("{}", tag.target_id());
payload.extend_from_slice(id.as_bytes());
payload.extend_from_slice(b"\ntype commit\ntag ");
payload.extend_from_slice(tag.name_bytes());
payload.push(b'\n');
if let Some(tagger) = tag.tagger() {
name = tagger.name().map(ToString::to_string);
email = tagger.email().map(ToString::to_string);
let when = tagger.when();
let unix_timestamp_seconds = when.seconds();
let offset_hours = when.offset_minutes() / 60;
let offset_minutes = when.offset_minutes() % 60;
let tagger = format!(
"tagger {} {} {:+03}{:02}\n",
tagger, unix_timestamp_seconds, offset_hours, offset_minutes
payload.extend_from_slice(tagger.as_bytes());
if let Some(dt) = NaiveDateTime::from_timestamp_opt(unix_timestamp_seconds, 0) {
datetime =
FixedOffset::east_opt(when.offset_minutes() * 60).map(|o| o.from_utc_datetime(&dt));
payload.extend_from_slice(message);
let oid = format!("{}", tag.id());
tag: tag_name.to_string(),
payload,
/// Check the digital signature on the tag.
/// Return the ids of the keys with which the signature was created.
/// Return an empty vector if the signature could not be checked due to
/// missing keys. The missing keys are added to the supplied vector.
/// Return an error if the check failed.
fn check_tag_signature(
signature: &TagSignature,
certs: &[Cert],
missing_keys: &mut Vec<KeyHandle>,
) -> Result<Vec<(KeyHandle, Cert)>> {
match check_signature(certs, &signature.signature) {
Ok(validated_signature) => Ok(validated_signature.certificates),
Err(e) => {
// recover the errors collected by the helper
if let Some(source) = e.downcast_ref::<SignatureErrors>() {
let mut only_missing_key = true;
for e in &source.0 {
if let Some(source) = e.downcast_ref::<MissingKeyError>() {
if let Some(missing_key) = source.missing_key() {
if !missing_keys.contains(&missing_key) {
missing_keys.push(missing_key);
only_missing_key = false;
if only_missing_key {
Ok(Vec::new())
Err(e)
#[derive(Serialize)]
struct TagSignature {
#[serde(skip_serializing_if = "Option::is_none")]
signature: PgpSignature,
fn get_signatures(repo_dir: &str) -> Result<Vec<TagSignature>> {
let mut signatures = Vec::new();
let repo = git2::Repository::open(repo_dir)?;
for reference in repo.references()? {
let reference = reference?;
if let Ok(commit) = reference.peel_to_commit() {
if let Some((_msg, _sig)) = get_pgp_signature(Some(commit.message_bytes())) {
// println!("got commit {}", msg);
if let Ok(tag) = reference.peel_to_tag() {
if let Some(signature) = get_tag_signature(&tag) {
signatures.push(signature);
let mut revwalk = repo.revwalk()?;
revwalk.push_head()?;
let mut count = 0;
for oid in revwalk {
if let Ok(commit) = oid.and_then(|o| repo.find_commit(o)) {
if let Some(signature) = get_commit_signature(&commit) {
count += 1;
eprintln!("count {}", count);
Ok(signatures)
fn check_signatures(certs: &[Cert], signatures: Vec<TagSignature>) -> Result<Vec<TagSignature>> {
let mut missing_keys = Vec::new();
let mut valid_signatures = Vec::new();
for signature in signatures {
if let Ok(key_ids) = check_tag_signature(&signature, certs, &mut missing_keys) {
let checked = !key_ids.is_empty();
if checked {
valid_signatures.push(signature);
println!("missing keys: {}", missing_keys.len());
Ok(valid_signatures)
fn main() -> Result<()> {
let mut args = std::env::args();
let _ = args.next().unwrap();
let repo_dir = args.next().unwrap();
let gpg_dir = PathBuf::from(args.next().unwrap());
let certs = load_keys(&gpg_dir)?;
let signatures = get_signatures(&repo_dir)?;
println!("got {} signatures", signatures.len());
let valid_signatures = check_signatures(&certs, signatures)?;
let json = serde_json::to_string_pretty(&valid_signatures)?;
eprintln!("{}", json);
Ok(())