summaryrefslogtreecommitdiff
path: root/parabola_repolint/linter_checks/package_signature.py
blob: 7c62adc5c7d1dd1628fdf713722fb686c6a87e3b (plain)
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
'''
this are linter checks for package signatures
'''

import base64
import logging
import datetime

from parabola_repolint.linter import LinterIssue, LinterCheckBase, LinterCheckType


class SigningKeyExpiry(LinterCheckBase):
    '''
  for the list of signing keys and subkeys in parabola.gpg that are used to sign
  packages in the repos, check whether they are expired, or are about to expire.
  This check reports an issue for any expired signing key in the keyring, as well
  as any key that is going to expire within the next 90 days, indicating that the
  key should be extended and the keyring rebuilt to avoid user-facing issues on
  system updates.
'''

    name = 'signing_key_expiry'
    check_type = LinterCheckType.SIGNING_KEY

    header = 'signing keys expired or about to expire'

    # pylint: disable=no-self-use
    def check(self, key):
        ''' run the check '''
        if not key['packages']:
            return
        if not key['expires']:
            return

        expires = datetime.datetime.utcfromtimestamp(int(key['expires']))
        time = expires.strftime("%Y-%m-%d")

        reason = None

        if expires < datetime.datetime.now():
            reason = 'expired %s' % time
        elif expires <= datetime.datetime.now() + datetime.timedelta(days=90):
            reason = 'expires %s' % time

        if reason is not None:
            if 'master_key' in key:
                reason += ' (subkey of %s)' % key['master_key']
            raise LinterIssue('%s: %s (signed %i)', key['keyid'], reason, len(key['packages']))


class MasterKeyExpiry(LinterCheckBase):
    '''
  for the list of master keys in parabola.gpg, check whether they are expired, or
  are about to expire. This check reports an issue for any expired master key in
  the keyring, as well as any key that is going to expire within the next 90
  days, indicating that the key should be extended and the keyring rebuilt to
  avoid user-facing issues on system updates.
'''

    name = 'master_key_expiry'
    check_type = LinterCheckType.MASTER_KEY

    header = 'master keys expired or about to expire'

    # pylint: disable=no-self-use
    def check(self, key):
        ''' run the check '''
        if not key['expires']:
            return

        expires = datetime.datetime.utcfromtimestamp(int(key['expires']))
        time = expires.strftime("%Y-%m-%d")

        reason = None

        if expires < datetime.datetime.now():
            reason = 'expired %s' % time
        elif expires <= datetime.datetime.now() + datetime.timedelta(days=90):
            reason = 'expires %s' % time

        if reason is not None:
            raise LinterIssue('%s: %s', key['keyid'], reason)


class PkgEntrySignatureMismatch(LinterCheckBase):
    '''
  for the list of entries in the repo.db's, check whether the signature stored in
  the repository matches the signature stored in the detached signature file of
  the corresponding built package. This check reports an issue whenever the
  signature in the repo.db reports a different key then what was used to produce
  the built package signature.
'''

    name = 'pkgentry_signature_mismatch'
    check_type = LinterCheckType.PKGENTRY

    header = 'repo.db entries with mismatched signing keys'

    # pylint: disable=no-self-use
    def check(self, pkgentry):
        ''' run the check '''
        if not pkgentry.pkgfile:
            return

        key1 = pkgentry.pkgfile.siginfo['key_id'].upper()
        key2 = base64.b64decode(pkgentry.pgpsig)

        # quick & dirty gpg packet parser
        # https://tools.ietf.org/search/rfc4880
        header = key2[0]
        assert header & 0x80 == 0x80
        assert header & 0x40 == 0x00
        assert header & 0x03 == 0x01
        assert header & 0x1C == 0x08

        version = key2[3]
        assert version == 0x04

        hashed_len = key2[7] * 256 + key2[8]
        unhashed_len = key2[9 + hashed_len] * 256 + key2[10 + hashed_len]

        data = key2[11 + hashed_len:11 + hashed_len + unhashed_len]
        assert data[1] == 0x10 # issuer
        key2 = data[2:10].hex().upper()

        if key1 != key2:
            raise LinterIssue('%s: %s != %s', pkgentry, key1, key2)


class PkgFileInvalidSignature(LinterCheckBase):
    '''
  this check validates the package signature against the pacman keyrings. It
  reports an issue whenever a package is signed by an unknown key, that is not
  part of the keyring, or by a key that has expired.
'''

    name = 'pkgfile_invalid_signature'
    check_type = LinterCheckType.PKGFILE

    header = 'packages with invalid signatures'

    def check(self, package):
        ''' run the check '''
        verify = package.siginfo

        key = self._cache.key_cache.get(verify['key_id'], None)
        if key is None:
            raise LinterIssue('%s: no public key: %s', package, verify['key_id'])

        if key['expires']:
            expires = datetime.datetime.utcfromtimestamp(int(key['expires']))
            if expires < datetime.datetime.now():
                raise LinterIssue('%s: signing key expired (%s)', package, verify['key_id'])

        if not verify['valid']:
            # if this is triggered, add more cases here.
            logging.warning('%s: unknown gpg invalidity: %s', package, verify)
            raise LinterIssue('%s: invalid signature', package)