rpgpie/
signature.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Handling of OpenPGP signatures.

use std::io;
use std::ops::Add;

use chrono::{DateTime, Utc};
use pgp::packet::{Packet, RevocationCode, SignatureType, SubpacketData};
use pgp::ser::Serialize;
use pgp::{Deserializable, Signature, StandaloneSignature};

use crate::policy::{accept_for_signatures, acceptable_hash_algorithm};
use crate::Error;

fn is_revocation(sig: &Signature) -> bool {
    match sig.typ() {
        SignatureType::KeyRevocation
        | SignatureType::CertRevocation
        | SignatureType::SubkeyRevocation => {
            // this is a revocation

            true // unless explicitly marked as "soft" (by the reason code), we consider a revocation to be "hard"
        }
        _ => false, // this signature is not a revocation
    }
}

fn is_hard_revocation(sig: &Signature) -> bool {
    // FIXME: DRY with is_revocation
    match sig.typ() {
        SignatureType::KeyRevocation
        | SignatureType::CertRevocation
        | SignatureType::SubkeyRevocation => {
            // this is a revocation, but is it a hard revocation?

            match sig.revocation_reason_code() {
                Some(RevocationCode::KeyRetired)
                | Some(RevocationCode::CertUserIdInvalid)
                | Some(RevocationCode::KeySuperseded) => {
                    return false; // these are soft revocation codes
                }
                _ => {}
            }

            true // unless explicitly marked as "soft" (by the reason code), we consider a revocation to be "hard"
        }
        _ => false, // this signature is not a revocation
    }
}

/// Reports if a signature meets basic acceptability criteria, including the cryptographic
/// primitives it uses:
///
/// - Rejects signatures that have no creation time stamp set in the hashed area
/// - Rejects signatures that have a creation time stamp in the future
/// - Rejects signatures that contain unknown critical subpackets
///
/// - Rejects signatures based on weak cryptographic mechanisms, based on (claimed) signature creation time:
///
/// md5 hashes are considered broken (effective January 1, 2010, based on the signature creation time).
/// We consider a more recently dated md5-based signature equally broken as one with an invalid cryptographic hash digest.
///
/// sha1 hashes for data signatures are considered broken effective January 1, 2014;
/// sha1 hashes for other signature types are considered broken effective February 1, 2023.
///
/// DSA signatures are considered unacceptable starting February 3, 2023.
///
/// (Rejecting signatures that are technically correct, but use broken primitives is a defensive
/// tradeoff: we consider legacy signatures that were made after a cutoff time as either attacks or
/// mistakes.)
pub(crate) fn signature_acceptable(sig: &Signature) -> bool {
    // FIXME: break this fn up, it does too many different things

    let Some(sig_creation_time) = sig.created() else {
        return false;
    };

    // A signature with a future creation time is not currently valid
    let now: DateTime<Utc> = chrono::offset::Utc::now();
    if *sig_creation_time > now {
        return false;
    }

    // critical unknown subpackets or notations invalidate a signature
    for sp in &sig.config.hashed_subpackets {
        if sp.is_critical {
            if let SubpacketData::Notation(_notation) = &sp.data {
                // Unknown critical notation (by default)
                // FIXME: how would an application use critical notations? initialize rpgpie with a good-list?
                return false;
            }
        }
    }

    let data_sig = sig.typ() == SignatureType::Binary || sig.typ() == SignatureType::Text;

    // reject signature if our policy rejects the hash algorithm at signature creation time
    if !acceptable_hash_algorithm(&sig.config.hash_alg, sig_creation_time, data_sig) {
        return false;
    }

    // reject signature if our policy rejects the signature's public key algorithm at creation time
    if !accept_for_signatures(sig.config.pub_alg, sig_creation_time) {
        return false;
    }

    true
}

pub(crate) fn not_older_than(sig: &Signature, created: &DateTime<Utc>) -> bool {
    sig.created()
        .is_some_and(|sig_created| sig_created >= created)
}

/// How long is `sig` valid?
///
/// Some(dt): valid until `dt`
/// None: unlimited validity
///
/// NOTE: Also returns `None` if `sig` has no creation time.
pub(crate) fn validity_end(sig: &Signature) -> Option<DateTime<Utc>> {
    let Some(sig_creation) = sig.created() else {
        // This is an error case, but we don't handle it here.
        // Callers are expected to handle it independently of this fn.
        return None;
    };

    if let Some(sig_expiration) = sig.signature_expiration_time() {
        if sig_expiration.num_seconds() != 0 {
            Some(sig_creation.add(*sig_expiration))
        } else {
            None
        }
    } else {
        None
    }
}

/// Check temporal validity of a signature
pub(crate) fn is_signature_valid_at(
    sig: &Signature,
    key_creation: &DateTime<Utc>,
    reference: &DateTime<Utc>,
) -> bool {
    if let Some(creation) = sig.created() {
        // If the signature is created after the reference time, it is invalid at reference time
        if creation > reference {
            return false;
        }

        // If the signature expires before the reference time, the signature is invalid
        if let Some(sig_exp) = validity_end(sig) {
            if sig_exp < *reference {
                return false;
            }
        }

        // If the key expires and expiration is before the reference time, the signature is invalid
        if let Some(key_exp) = sig.key_expiration_time() {
            if key_exp.num_seconds() != 0 && (key_creation.add(*key_exp) < *reference) {
                return false;
            }
        }

        true
    } else {
        // A signature with unset creation time is invalid
        false
    }
}

/// Read a list of Signatures from an input
pub fn load<R: io::Read>(source: &mut R) -> Result<Vec<Signature>, Error> {
    let (iter, _header) = StandaloneSignature::from_reader_many(source)?;

    let mut sigs = vec![];

    for res in iter {
        match res {
            Ok(sig) => sigs.push(sig.signature),
            Err(e) => return Err(Error::Message(format!("Bad data: {:?}", e))),
        }
    }

    Ok(sigs)
}

/// Write a list of Signatures to an output
pub fn save(
    signatures: &[Signature],
    armored: bool,
    mut sink: &mut dyn io::Write,
) -> Result<(), Error> {
    if armored {
        let packets: Vec<_> = signatures.iter().map(|s| Packet::from(s.clone())).collect();

        pgp::armor::write(
            &packets,
            pgp::armor::BlockType::Signature,
            &mut sink,
            None,
            true,
        )?;
    } else {
        for s in signatures {
            let p = Packet::from(s.clone());
            p.to_writer(&mut sink)?;
        }
    }

    Ok(())
}

pub(crate) struct SigStack<'inner> {
    hard: Vec<&'inner Signature>,
    soft: Vec<&'inner Signature>,
    regular: Vec<&'inner Signature>,

    #[allow(dead_code)]
    invalid: Vec<&'inner Signature>,

    #[allow(dead_code)]
    third_party: Vec<&'inner Signature>,
}

impl<'a> FromIterator<&'a Signature> for SigStack<'a> {
    fn from_iter<T: IntoIterator<Item = &'a Signature>>(iter: T) -> Self {
        let mut hard = vec![];
        let mut soft = vec![];
        let mut regular = vec![];
        let mut invalid = vec![];
        let third_party = vec![];

        // FIXME: pre-process?
        // - Different Enum variants based on hard-revoked vs. not?
        // - Sort signatures by creation time?
        // - Calculate time spans for validity of the component?

        for sig in iter.into_iter() {
            if is_hard_revocation(sig) {
                hard.push(sig)
            } else if is_revocation(sig) {
                soft.push(sig)
            } else if signature_acceptable(sig) {
                regular.push(sig)
            } else {
                invalid.push(sig)

                // FIXME: handle third party sigs
            }
        }

        Self {
            hard,
            soft,
            regular,
            invalid,
            third_party,
        }
    }
}

impl<'a> SigStack<'a> {
    // FIXME: this is an odd hack, and kind of expensive to make.
    // This probably should be replaced with a more reasonable access mechanism.
    #[allow(dead_code)]
    pub fn all(&self) -> Vec<&Signature> {
        // TODO: always return two sets: "valid" and "invalid"?
        //
        // -> for hard revoked, all other sigs are "invalid" (because they are suspect and can't be
        // relied on, they are only of informational value)
        //
        // -> for non-hard-revoked, only "self.invalid" are considered "invalid"? (unsound
        // signatures of all kinds might be of interest to viewers, but shouldn't be relied on for
        // anything)

        let mut all: Vec<&Signature> = vec![];
        self.hard.iter().for_each(|&s| all.push(s));
        self.soft.iter().for_each(|&s| all.push(s));
        self.regular.iter().for_each(|&s| all.push(s));

        // FIXME: handle "invalid" sigs?

        // FIXME: handle third party sigs?

        // sort by creation, newest first
        // FIXME: missing creation times are not legal, do we want to handle them here?
        all.sort_by(|a, b| b.created().cmp(&a.created()));

        all
    }

    /// Get the latest active signature in this stack.
    pub fn active(&self) -> Option<&'a Signature> {
        self.active_at(None)
    }

    /// Get the currently active signature in this stack at a reference time.
    /// If the reference time is `None`, then the latest signature.
    pub(crate) fn active_at(&self, reference: Option<&DateTime<Utc>>) -> Option<&'a Signature> {
        log::debug!("search active_at: {:?}", reference);

        // If there is any hard revocation, that is always active
        // (if there are multiple, picking a specific one is probably pointless)
        if let Some(&hard) = self.hard.first() {
            log::debug!(
                " found hard: {}",
                hard.created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
            return Some(hard);
        }

        // If any soft revocations exist that are created <= `reference`, we return the latest of them.
        let mut latest_soft: Option<&Signature> = None;
        self.soft
            .iter()
            .filter(|&&s| {
                if let Some(reference) = reference {
                    if let Some(sig_created) = s.created() {
                        // only consider signatures that were created before the reference time
                        sig_created <= reference
                    } else {
                        false // signature has no creation time, we ignore it
                    }
                } else {
                    true
                }
            })
            .for_each(|s| {
                // replace "latest_soft" with any newer soft revocation
                if let Some(cur) = latest_soft {
                    if let Some(sig_created) = s.created() {
                        if let Some(cur_created) = cur.created() {
                            if cur_created < sig_created {
                                latest_soft = Some(s);
                            }
                        }
                    }
                } else {
                    latest_soft = Some(s);
                }
            });

        if let Some(latest_soft) = latest_soft {
            log::debug!(
                " found soft: {}",
                latest_soft
                    .created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
            return Some(latest_soft);
        }

        // Otherwise we return the latest regular signature, if any
        let mut latest: Option<&Signature> = None;

        self.regular
            .iter()
            .filter(|s| {
                if let Some(reference) = reference {
                    if let Some(expired) = validity_end(s) {
                        // only consider signatures that are valid longer than the reference time
                        expired > *reference
                    } else {
                        true
                    }
                } else {
                    true
                }
            })
            .filter(|&&s| {
                if let Some(reference) = reference {
                    if let Some(sig_created) = s.created() {
                        // only consider signatures that were created before the reference time
                        sig_created <= reference
                    } else {
                        false // signature has no creation time, we ignore it
                    }
                } else {
                    true
                }
            })
            .for_each(|s| {
                // replace "latest" with any newer Signatures
                if let Some(cur) = latest {
                    if let Some(sig_created) = s.created() {
                        if let Some(cur_created) = cur.created() {
                            if cur_created < sig_created {
                                latest = Some(s);
                            }
                        }
                    }
                } else {
                    latest = Some(s);
                }
            });

        if let Some(latest) = latest {
            log::debug!(
                " latest regular: {}",
                latest
                    .created()
                    .map(|dt| format!("{:?}", dt))
                    .unwrap_or("<no creation time>".to_string())
            );
        }

        latest
    }

    /// Does this signature stack contain a (non-revocation) Signature that is temporally valid at
    /// `reference`?
    pub fn has_valid_binding_at(
        &self,
        reference: &DateTime<Utc>,
        key_creation: &DateTime<Utc>, // FIXME: factor out?
    ) -> bool {
        // FIXME: we could search more efficiently in large stacks, if the stack were sorted (maybe later)
        self.regular
            .iter()
            .any(|s| is_signature_valid_at(s, key_creation, reference))
    }

    pub(crate) fn revoked_at(&self, reference: &DateTime<Utc>) -> bool {
        self.contains_hard_revocation() || self.soft_revoked_at(reference)
    }

    /// CAUTION: this fn doesn't consider hard revocations!
    fn soft_revoked_at(&self, reference: &DateTime<Utc>) -> bool {
        log::debug!("soft_revoked_at {:?}", reference);
        if self.soft.iter().any(|&s| match s.created() {
            None => false, // we just ignore signatures without creation time
            Some(created) => created <= reference,
        }) {
            return true; // We consider this revocation active
        }

        false
    }

    fn contains_hard_revocation(&self) -> bool {
        !self.hard.is_empty()
    }
}