1
use crate::gpg::{check_signature, MissingKeyError, PgpSignature};
2
use chrono::{DateTime, FixedOffset, NaiveDateTime, TimeZone};
3
use git2::Repository;
4
use sequoia_openpgp::{Cert, KeyHandle};
5
use serde::Serialize;
6

            
7
type Result<T> = anyhow::Result<T>;
8

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

            
14
140
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
15
140
    haystack
16
140
        .windows(needle.len())
17
27140
        .position(|window| window == needle)
18
140
}
19

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

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

            
54
struct PartialGitSignature {
55
    oid: String,
56
    parent: String,
57
    name: Option<String>,
58
    email: Option<String>,
59
    datetime: Option<DateTime<FixedOffset>>,
60
}
61

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

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

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

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

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

            
149
/// Retrieve the signature of the commit if it has one.
150
1240
fn get_commit_signature(commit: &git2::Commit) -> Option<TagSignature> {
151
1240
    let msg = commit.raw_header().map(|b| b.as_bytes());
152
1240
    parse_signature(msg)
153
1240
}
154

            
155
1240
fn parse_signature(msg: Option<&[u8]>) -> Option<TagSignature> {
156
1240
    let (message, signature) = get_pgp_signature(msg)?;
157
120
    if let Some(partial) = parse_payload(message) {
158
120
        Some(TagSignature {
159
120
            tag: partial.parent,
160
120
            oid: partial.oid,
161
120
            name: partial.name,
162
120
            email: partial.email,
163
120
            datetime: partial.datetime,
164
120
            signature: PgpSignature {
165
120
                payload: message.to_vec(),
166
120
                signature: signature.to_vec(),
167
120
            },
168
120
        })
169
    } else {
170
        /*
171
        println!(
172
            "could not parse message '{}'",
173
            String::from_utf8_lossy(message)
174
        );
175
        */
176
        None
177
    }
178
1240
}
179

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

            
229
4
#[derive(Serialize, PartialEq)]
230
pub struct TagSignature {
231
    tag: String,
232
    oid: String,
233
    #[serde(skip_serializing_if = "Option::is_none")]
234
    name: Option<String>,
235
    #[serde(skip_serializing_if = "Option::is_none")]
236
    email: Option<String>,
237
    #[serde(skip_serializing_if = "Option::is_none")]
238
    datetime: Option<DateTime<FixedOffset>>,
239
    pub signature: PgpSignature,
240
}
241

            
242
20
pub fn get_signatures(repo: &Repository) -> Result<Vec<TagSignature>> {
243
20
    let mut signatures = Vec::new();
244
130
    for reference in repo.references()? {
245
130
        let reference = reference?;
246
130
        if let Ok(commit) = reference.peel_to_commit() {
247
130
            if let Some(signature) = get_commit_signature(&commit) {
248
40
                signatures.push(signature);
249
90
            }
250
        }
251
130
        if let Ok(tag) = reference.peel_to_tag() {
252
20
            if let Some(signature) = get_tag_signature(&tag) {
253
20
                signatures.push(signature);
254
20
            }
255
110
        }
256
    }
257
20
    let mut revwalk = repo.revwalk()?;
258
20
    revwalk.push_head()?;
259
1130
    for oid in revwalk {
260
1110
        if let Ok(commit) = oid.and_then(|o| repo.find_commit(o)) {
261
1110
            if let Some(signature) = get_commit_signature(&commit) {
262
80
                signatures.push(signature);
263
1030
            }
264
        }
265
    }
266
20
    Ok(signatures)
267
20
}
268

            
269
/// Check the digital signature on the tag.
270
/// Return the ids of the keys with which the signature was created.
271
/// Return an empty vector if the signature could not be checked due to
272
/// missing keys. The missing keys are added to the supplied vector.
273
/// Return an error if the check failed.
274
140
fn check_tag_signature(
275
140
    signature: &TagSignature,
276
140
    certs: &[Cert],
277
140
    missing_keys: &mut Vec<KeyHandle>,
278
140
) -> Result<Vec<(KeyHandle, Cert)>> {
279
140
    match check_signature(certs, &signature.signature) {
280
140
        Ok(validated_signature) => {
281
270
            for e in &validated_signature.errors {
282
130
                if let Some(source) = e.downcast_ref::<MissingKeyError>() {
283
120
                    if let Some(missing_key) = source.missing_key() {
284
120
                        if !missing_keys.contains(&missing_key) {
285
30
                            missing_keys.push(missing_key);
286
90
                        }
287
                    }
288
10
                }
289
            }
290
140
            Ok(validated_signature.certificates)
291
        }
292
        Err(e) => Err(e),
293
    }
294
140
}
295

            
296
pub struct CheckSignaturesResult {
297
    pub valid_signatures: Vec<TagSignature>,
298
    pub found_keys: Vec<(KeyHandle, Cert)>,
299
    pub missing_keys: Vec<KeyHandle>,
300
}
301

            
302
20
pub fn check_signatures(
303
20
    certs: &[Cert],
304
20
    signatures: Vec<TagSignature>,
305
20
) -> Result<CheckSignaturesResult> {
306
20
    let mut missing_keys = Vec::new();
307
20
    let mut found_keys = Vec::new();
308
20
    let mut valid_signatures = Vec::new();
309
160
    for signature in signatures {
310
140
        if let Ok(mut key_ids) = check_tag_signature(&signature, certs, &mut missing_keys) {
311
140
            let checked = !key_ids.is_empty();
312
140
            if checked {
313
20
                valid_signatures.push(signature);
314
20
                found_keys.append(&mut key_ids);
315
120
            }
316
        }
317
    }
318
20
    Ok(CheckSignaturesResult {
319
20
        valid_signatures,
320
20
        found_keys,
321
20
        missing_keys,
322
20
    })
323
20
}