1# crypto.py
2# Keys and signatures.
3#
4# Copyright (C) 2014  Red Hat, Inc.
5#
6# This copyrighted material is made available to anyone wishing to use,
7# modify, copy, or redistribute it subject to the terms and conditions of
8# the GNU General Public License v.2, or (at your option) any later version.
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY expressed or implied, including the implied warranties of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
12# Public License for more details.  You should have received a copy of the
13# GNU General Public License along with this program; if not, write to the
14# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
16# source code or documentation are not subject to the GNU General Public
17# License and may only be used or replicated with the express permission of
18# Red Hat, Inc.
19#
20
21from __future__ import print_function
22from __future__ import absolute_import
23from __future__ import unicode_literals
24from dnf.i18n import _
25import contextlib
26import dnf.pycomp
27import dnf.util
28import dnf.yum.misc
29import io
30import logging
31import os
32import tempfile
33
34try:
35    from gpg import Context
36    from gpg import Data
37except ImportError:
38    import gpgme
39
40
41    class Context(object):
42        def __init__(self):
43            self.__dict__["ctx"] = gpgme.Context()
44
45        def __enter__(self):
46            return self
47
48        def __exit__(self, type, value, tb):
49            pass
50
51        @property
52        def armor(self):
53            return self.ctx.armor
54
55        @armor.setter
56        def armor(self, value):
57            self.ctx.armor = value
58
59        def op_import(self, key_fo):
60            if isinstance(key_fo, basestring):
61                key_fo = io.BytesIO(key_fo)
62            self.ctx.import_(key_fo)
63
64        def op_export(self, pattern, mode, keydata):
65            self.ctx.export(pattern, keydata)
66
67        def __getattr__(self, name):
68            return getattr(self.ctx, name)
69
70
71    class Data(object):
72        def __init__(self):
73            self.__dict__["buf"] = io.BytesIO()
74
75        def __enter__(self):
76            return self
77
78        def __exit__(self, type, value, tb):
79            pass
80
81        def read(self):
82            return self.buf.getvalue()
83
84        def __getattr__(self, name):
85            return getattr(self.buf, name)
86
87
88GPG_HOME_ENV = 'GNUPGHOME'
89logger = logging.getLogger('dnf')
90
91
92def _extract_signing_subkey(key):
93    return dnf.util.first(subkey for subkey in key.subkeys if subkey.can_sign)
94
95
96def _printable_fingerprint(fpr_hex):
97    segments = (fpr_hex[i:i + 4] for i in range(0, len(fpr_hex), 4))
98    return " ".join(segments)
99
100
101def import_repo_keys(repo):
102    gpgdir = repo._pubring_dir
103    known_keys = keyids_from_pubring(gpgdir)
104    for keyurl in repo.gpgkey:
105        for keyinfo in retrieve(keyurl, repo):
106            keyid = keyinfo.id_
107            if keyid in known_keys:
108                logger.debug(_('repo %s: 0x%s already imported'), repo.id, keyid)
109                continue
110            if not repo._key_import._confirm(keyinfo):
111                continue
112            dnf.yum.misc.import_key_to_pubring(
113                keyinfo.raw_key, keyinfo.short_id, gpgdir=gpgdir,
114                make_ro_copy=False)
115            logger.debug(_('repo %s: imported key 0x%s.'), repo.id, keyid)
116
117
118def keyids_from_pubring(gpgdir):
119    if not os.path.exists(gpgdir):
120        return []
121
122    with pubring_dir(gpgdir), Context() as ctx:
123        keyids = []
124        for k in ctx.keylist():
125            subkey = _extract_signing_subkey(k)
126            if subkey is not None:
127                keyids.append(subkey.keyid)
128        return keyids
129
130
131def log_key_import(keyinfo):
132    msg = (_('Importing GPG key 0x%s:\n'
133             ' Userid     : "%s"\n'
134             ' Fingerprint: %s\n'
135             ' From       : %s') %
136           (keyinfo.short_id, keyinfo.userid,
137            _printable_fingerprint(keyinfo.fingerprint),
138            keyinfo.url.replace("file://", "")))
139    logger.critical("%s", msg)
140
141
142def log_dns_key_import(keyinfo, dns_result):
143    log_key_import(keyinfo)
144    if dns_result == dnf.dnssec.Validity.VALID:
145        logger.critical(_('Verified using DNS record with DNSSEC signature.'))
146    else:
147        logger.critical(_('NOT verified using DNS record.'))
148
149@contextlib.contextmanager
150def pubring_dir(pubring_dir):
151    orig = os.environ.get(GPG_HOME_ENV, None)
152    os.environ[GPG_HOME_ENV] = pubring_dir
153    try:
154        yield
155    finally:
156        if orig is None:
157            del os.environ[GPG_HOME_ENV]
158        else:
159            os.environ[GPG_HOME_ENV] = orig
160
161
162def rawkey2infos(key_fo):
163    pb_dir = tempfile.mkdtemp()
164    keyinfos = []
165    with pubring_dir(pb_dir), Context() as ctx:
166        ctx.op_import(key_fo)
167        for key in ctx.keylist():
168            subkey = _extract_signing_subkey(key)
169            if subkey is None:
170                continue
171            keyinfos.append(Key(key, subkey))
172        ctx.armor = True
173        for info in keyinfos:
174            with Data() as sink:
175                ctx.op_export(info.id_, 0, sink)
176                sink.seek(0, os.SEEK_SET)
177                info.raw_key = sink.read()
178    dnf.util.rm_rf(pb_dir)
179    return keyinfos
180
181
182def retrieve(keyurl, repo=None):
183    if keyurl.startswith('http:'):
184        logger.warning(_("retrieving repo key for %s unencrypted from %s"), repo.id, keyurl)
185    with dnf.util._urlopen(keyurl, repo=repo) as handle:
186        keyinfos = rawkey2infos(handle)
187    for keyinfo in keyinfos:
188        keyinfo.url = keyurl
189    return keyinfos
190
191
192class Key(object):
193    def __init__(self, key, subkey):
194        self.id_ = subkey.keyid
195        self.fingerprint = subkey.fpr
196        self.raw_key = None
197        self.timestamp = subkey.timestamp
198        self.url = None
199        self.userid = key.uids[0].uid
200
201    @property
202    def short_id(self):
203        rj = '0' if dnf.pycomp.PY3 else b'0'
204        return self.id_[-8:].rjust(8, rj)
205
206    @property
207    def rpm_id(self):
208        return self.short_id.lower()
209