1
use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone};
2
use review_harvest::gpg::{
3
    check_signature, load_keys, MissingKeyError, PgpSignature, SignatureErrors,
4
};
5
use sequoia_openpgp::{Cert, KeyHandle};
6
use serde::Serialize;
7
/// A simple application that lists digital signatures from a git repository
8
use std::path::PathBuf;
9

            
10
type Result<T> = anyhow::Result<T>;
11

            
12
const BEGIN_PGP_SIGNATURE: &[u8] = b"-----BEGIN PGP SIGNATURE-----\n";
13
const END_PGP_SIGNATURE: &[u8] = b"-----END PGP SIGNATURE-----";
14
const BEGIN_PGP_MESSAGE: &[u8] = b"-----BEGIN PGP MESSAGE-----\n";
15
const END_PGP_MESSAGE: &[u8] = b"-----END PGP MESSAGE-----";
16

            
17
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
18
    haystack
19
        .windows(needle.len())
20
        .position(|window| window == needle)
21
}
22

            
23
fn find_begin_end<'a>(
24
    msg: &'a [u8],
25
    begin: &'static [u8],
26
    end: &'static [u8],
27
) -> Option<(&'a [u8], &'a [u8])> {
28
    if msg.len() < begin.len() + end.len() + 1 {
29
        return None;
30
    }
31
    let maybe_end = if msg[msg.len() - 1] == b'\n' {
32
        &msg[msg.len() - end.len() - 1..msg.len() - 1]
33
    } else {
34
        &msg[msg.len() - end.len()..]
35
    };
36
    if maybe_end != end {
37
        return None;
38
    }
39
    if let Some(first) = find_subsequence(msg, begin) {
40
        let signature = &msg[first..];
41
        return Some((&msg[..first], signature));
42
    }
43
    None
44
}
45

            
46
fn get_pgp_signature(msg: Option<&[u8]>) -> Option<(&[u8], &[u8])> {
47
    let msg = msg?;
48
    if let Some(r) = find_begin_end(msg, BEGIN_PGP_SIGNATURE, END_PGP_SIGNATURE) {
49
        return Some(r);
50
    }
51
    if let Some(r) = find_begin_end(msg, BEGIN_PGP_MESSAGE, END_PGP_MESSAGE) {
52
        return Some(r);
53
    }
54
    None
55
}
56

            
57
struct PartialGitSignature {
58
    tag: String,
59
    oid: String,
60
    name: Option<String>,
61
    email: Option<String>,
62
    datetime: Option<DateTime<FixedOffset>>,
63
}
64

            
65
fn strip_prefix<'a>(bytes: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
66
    if bytes.len() >= prefix.len() && &bytes[..prefix.len()] == prefix {
67
        return Some(&bytes[prefix.len()..]);
68
    }
69
    None
70
}
71

            
72
fn split_at_newline(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
73
    if let Some(pos) = bytes.iter().position(|b| *b == b'\n') {
74
        return Some((&bytes[..pos], &bytes[pos + 1..]));
75
    }
76
    None
77
}
78

            
79
fn get_line_value<'a>(bytes: &'a [u8], prefix: &[u8]) -> Option<(&'a [u8], &'a [u8])> {
80
    let value = strip_prefix(bytes, prefix)?;
81
    split_at_newline(value)
82
}
83

            
84
/// Parse a value of the form
85
/// 'First Last <flast@kernel.org> 1493772623 +0430'
86
fn parse_tagger(tagger: &[u8]) -> Option<(String, String, DateTime<FixedOffset>)> {
87
    let gt_pos = tagger.iter().position(|b| *b == b'<')?;
88
    if gt_pos == 0 || tagger[gt_pos - 1] != b' ' {
89
        return None;
90
    }
91
    let name = &tagger[..gt_pos - 1];
92
    let rest = &tagger[gt_pos + 1..];
93
    let lt_pos = rest.iter().position(|b| *b == b'>')?;
94
    if lt_pos + 1 == rest.len() || rest[lt_pos + 1] != b' ' {
95
        return None;
96
    }
97
    let email = &rest[..lt_pos];
98
    let rest = &rest[lt_pos + 2..];
99
    let space_pos = rest.iter().position(|b| *b == b' ')?;
100
    let seconds = &rest[..space_pos];
101
    let tz = &rest[space_pos + 1..];
102
    let name = String::from_utf8(name.to_vec()).ok()?;
103
    let email = String::from_utf8(email.to_vec()).ok()?;
104
    let unix_timestamp_seconds: i64 = std::str::from_utf8(seconds)
105
        .ok()
106
        .and_then(|s| s.parse::<i64>().ok())?;
107
    if tz.len() != 5 {
108
        return None;
109
    }
110
    let offset_hours = std::str::from_utf8(&tz[1..2])
111
        .ok()
112
        .and_then(|s| s.parse::<i32>().ok())?;
113
    let offset_minutes = std::str::from_utf8(&tz[3..4])
114
        .ok()
115
        .and_then(|s| s.parse::<i32>().ok())?;
116
    let offset_seconds = offset_hours * 3600 + offset_minutes * 60;
117
    let dt = NaiveDateTime::from_timestamp_opt(unix_timestamp_seconds, 0)?;
118
    let offset = if tz[0] == b'-' {
119
        FixedOffset::west_opt(offset_seconds)
120
    } else {
121
        FixedOffset::east_opt(offset_seconds)
122
    }?;
123
    let datetime = offset.from_utc_datetime(&dt);
124
    Some((name, email, datetime))
125
}
126

            
127
/// obtain name, email, date, time and tag from the text of the commit
128
fn parse_payload(payload: &[u8]) -> Option<PartialGitSignature> {
129
    let (object, remainder) = get_line_value(payload, b"object ")?;
130
    let oid = String::from_utf8(object.to_vec()).ok()?;
131
    let (tag, remainder) = get_line_value(remainder, b"type commit\ntag ")
132
        .or_else(|| get_line_value(remainder, b"type tag\ntag "))?;
133
    let tag = String::from_utf8(tag.to_vec()).ok()?;
134
    let mut name = None;
135
    let mut email = None;
136
    let mut datetime = None;
137
    if let Some((tagger, _)) = get_line_value(remainder, b"tagger ") {
138
        if let Some((n, e, dt)) = parse_tagger(tagger) {
139
            name = Some(n);
140
            email = Some(e);
141
            datetime = Some(dt);
142
        }
143
    };
144
    Some(PartialGitSignature {
145
        tag,
146
        oid,
147
        name,
148
        email,
149
        datetime,
150
    })
151
}
152

            
153
/// Retrieve the signature of the tag if it has one.
154
fn get_commit_signature(commit: &git2::Commit) -> Option<TagSignature> {
155
    let mergetag = commit.header_field_bytes("mergetag").ok()?;
156
    let (message, signature) = get_pgp_signature(Some(&mergetag))?;
157
    if let Some(partial) = parse_payload(message) {
158
        Some(TagSignature {
159
            tag: partial.tag,
160
            oid: partial.oid,
161
            name: partial.name,
162
            email: partial.email,
163
            datetime: partial.datetime,
164
            signature: PgpSignature {
165
                payload: message.to_vec(),
166
                signature: signature.to_vec(),
167
            },
168
        })
169
    } else {
170
        println!(
171
            "could not parse message '{}'",
172
            String::from_utf8_lossy(message)
173
        );
174
        None
175
    }
176
}
177

            
178
/// Retrieve the signature of the tag if it has one.
179
fn get_tag_signature(tag: &git2::Tag) -> Option<TagSignature> {
180
    let (message, signature) = get_pgp_signature(tag.message_bytes())?;
181
    let tag_name = tag.name()?;
182
    // create a payload for checking the digital signature
183
    // see https://git-scm.com/docs/signature-format
184
    let mut payload: Vec<u8> = Vec::with_capacity(256 + message.len());
185
    payload.extend_from_slice(b"object ");
186
    let id = format!("{}", tag.target_id());
187
    payload.extend_from_slice(id.as_bytes());
188
    payload.extend_from_slice(b"\ntype commit\ntag ");
189
    payload.extend_from_slice(tag.name_bytes());
190
    payload.push(b'\n');
191
    let mut name = None;
192
    let mut email = None;
193
    let mut datetime = None;
194
    if let Some(tagger) = tag.tagger() {
195
        name = tagger.name().map(ToString::to_string);
196
        email = tagger.email().map(ToString::to_string);
197
        let when = tagger.when();
198
        let unix_timestamp_seconds = when.seconds();
199
        let offset_hours = when.offset_minutes() / 60;
200
        let offset_minutes = when.offset_minutes() % 60;
201
        let tagger = format!(
202
            "tagger {} {} {:+03}{:02}\n",
203
            tagger, unix_timestamp_seconds, offset_hours, offset_minutes
204
        );
205
        payload.extend_from_slice(tagger.as_bytes());
206
        if let Some(dt) = NaiveDateTime::from_timestamp_opt(unix_timestamp_seconds, 0) {
207
            datetime =
208
                FixedOffset::east_opt(when.offset_minutes() * 60).map(|o| o.from_utc_datetime(&dt));
209
        }
210
    }
211
    payload.push(b'\n');
212
    payload.extend_from_slice(message);
213
    let oid = format!("{}", tag.id());
214
    Some(TagSignature {
215
        tag: tag_name.to_string(),
216
        oid,
217
        name,
218
        email,
219
        datetime,
220
        signature: PgpSignature {
221
            payload,
222
            signature: signature.to_vec(),
223
        },
224
    })
225
}
226

            
227
/// Check the digital signature on the tag.
228
/// Return the ids of the keys with which the signature was created.
229
/// Return an empty vector if the signature could not be checked due to
230
/// missing keys. The missing keys are added to the supplied vector.
231
/// Return an error if the check failed.
232
fn check_tag_signature(
233
    signature: &TagSignature,
234
    certs: &[Cert],
235
    missing_keys: &mut Vec<KeyHandle>,
236
) -> Result<Vec<(KeyHandle, Cert)>> {
237
    match check_signature(certs, &signature.signature) {
238
        Ok(validated_signature) => Ok(validated_signature.certificates),
239
        Err(e) => {
240
            // recover the errors collected by the helper
241
            if let Some(source) = e.downcast_ref::<SignatureErrors>() {
242
                let mut only_missing_key = true;
243
                for e in &source.0 {
244
                    if let Some(source) = e.downcast_ref::<MissingKeyError>() {
245
                        if let Some(missing_key) = source.missing_key() {
246
                            if !missing_keys.contains(&missing_key) {
247
                                missing_keys.push(missing_key);
248
                            }
249
                        }
250
                    } else {
251
                        only_missing_key = false;
252
                    }
253
                }
254
                if only_missing_key {
255
                    Ok(Vec::new())
256
                } else {
257
                    Err(e)
258
                }
259
            } else {
260
                Err(e)
261
            }
262
        }
263
    }
264
}
265

            
266
#[derive(Serialize)]
267
struct TagSignature {
268
    tag: String,
269
    oid: String,
270
    #[serde(skip_serializing_if = "Option::is_none")]
271
    name: Option<String>,
272
    #[serde(skip_serializing_if = "Option::is_none")]
273
    email: Option<String>,
274
    #[serde(skip_serializing_if = "Option::is_none")]
275
    datetime: Option<DateTime<FixedOffset>>,
276
    signature: PgpSignature,
277
}
278

            
279
fn get_signatures(repo_dir: &str) -> Result<Vec<TagSignature>> {
280
    let mut signatures = Vec::new();
281
    let repo = git2::Repository::open(repo_dir)?;
282
    for reference in repo.references()? {
283
        let reference = reference?;
284
        if let Ok(commit) = reference.peel_to_commit() {
285
            if let Some((_msg, _sig)) = get_pgp_signature(Some(commit.message_bytes())) {
286
                // println!("got commit {}", msg);
287
            }
288
        }
289
        if let Ok(tag) = reference.peel_to_tag() {
290
            if let Some(signature) = get_tag_signature(&tag) {
291
                signatures.push(signature);
292
            }
293
        }
294
    }
295
    let mut revwalk = repo.revwalk()?;
296
    revwalk.push_head()?;
297
    let mut count = 0;
298
    for oid in revwalk {
299
        if let Ok(commit) = oid.and_then(|o| repo.find_commit(o)) {
300
            if let Some(signature) = get_commit_signature(&commit) {
301
                signatures.push(signature);
302
                count += 1;
303
            }
304
        }
305
    }
306
    eprintln!("count {}", count);
307
    Ok(signatures)
308
}
309

            
310
fn check_signatures(certs: &[Cert], signatures: Vec<TagSignature>) -> Result<Vec<TagSignature>> {
311
    let mut missing_keys = Vec::new();
312
    let mut valid_signatures = Vec::new();
313
    for signature in signatures {
314
        if let Ok(key_ids) = check_tag_signature(&signature, certs, &mut missing_keys) {
315
            let checked = !key_ids.is_empty();
316
            if checked {
317
                valid_signatures.push(signature);
318
            }
319
        }
320
    }
321
    println!("missing keys: {}", missing_keys.len());
322
    Ok(valid_signatures)
323
}
324

            
325
fn main() -> Result<()> {
326
    let mut args = std::env::args();
327
    let _ = args.next().unwrap();
328
    let repo_dir = args.next().unwrap();
329
    let gpg_dir = PathBuf::from(args.next().unwrap());
330
    let certs = load_keys(&gpg_dir)?;
331
    let signatures = get_signatures(&repo_dir)?;
332
    println!("got {} signatures", signatures.len());
333
    let valid_signatures = check_signatures(&certs, signatures)?;
334
    let json = serde_json::to_string_pretty(&valid_signatures)?;
335
    eprintln!("{}", json);
336
    Ok(())
337
}