-
Notifications
You must be signed in to change notification settings - Fork 416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
dns extension for automatic key management #1085
Conversation
dnf/base.py
Outdated
@@ -37,6 +37,7 @@ | |||
import dnf.conf | |||
import dnf.conf.read | |||
import dnf.crypto | |||
import dnf.dnssec.dnsseckeyverification as dnssec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either make from dnssec directory subpackage (using __init__.py
and then do from dnf import dnssec
), or if you do not plan to add more files into it, you can simply move dnssecverification.py to dnf/dnssec.py.
dnf/base.py
Outdated
# and therefore there is no way to know for sure (this is mainly for | ||
# backward compatibility) | ||
if dns_result == dnssec.Validity.VALID or \ | ||
dns_result == dnssec.Validity.PROVEN_NONEXISTENCE: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can shorten this to if dns_result in (dnssec.Validity.VALID, dnssec.Validity.PROVEN_NONEXISTENCE):
dnf/base.py
Outdated
if dns_result == dnssec.Validity.VALID or \ | ||
dns_result == dnssec.Validity.PROVEN_NONEXISTENCE: | ||
rc = True | ||
logger.info(dnssec.any_msg("The key has been approved.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All string literals should be translated from dnf.i18n import _
, _("The key has been approved.")
dnf/cli/main.py
Outdated
@@ -87,6 +89,7 @@ def _main(base, args, cli_class, option_parser): | |||
|
|||
# our core object for the cli | |||
base._logging._presetup() | |||
dnssec.RpmImportedKeys.check_imported_keys_validity(logger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of passing logger as parameter, you can do logger = logging.getLogger('dnf')
in dnssec module.
dnf/dnssec/dnsseckeyverification.py
Outdated
return digest + "." + tag + "." + domain | ||
else: | ||
# Error | ||
return "Error" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is strange - later you are not able to distinguish whether returned value is a regular location or an error. Consider returning None or raising Exception in case of incorrect input.
dnf/dnssec/dnsseckeyverification.py
Outdated
# type: Dict[str, Union[str, NoKey]] | ||
|
||
@staticmethod | ||
def __cache_hit(key_union, input_key_string): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, no two leading underscores please.
dnf/dnssec/dnsseckeyverification.py
Outdated
@staticmethod | ||
def __cache_miss(input_key): | ||
# type: (KeyInfo) -> Validity | ||
RR_TYPE_OPENPGPKEY = 61 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move constants definition to begining of module please
dnf/dnssec/dnsseckeyverification.py
Outdated
RR_TYPE_OPENPGPKEY = 61 | ||
ctx = unbound.ub_ctx() | ||
# TODO: is this the right place to put this file? | ||
ctx.config("/etc/dnf/libunbound.conf") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No please, it should be possible to use unbound module without any conf file.
dnf/dnssec/dnsseckeyverification.py
Outdated
p1 = subprocess.Popen(["rpm", "-q", "gpg-pubkey"], stdout=subprocess.PIPE) | ||
out = p1.communicate()[0] | ||
keys = out.decode().split('\n') | ||
return [x for x in keys if x.startswith('gpg-pubkey')] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like you to use rpm module instead. Something like (you will need to get dnf.base.Base instance somehow):
import dnf.rpm.transaction
ts = dnf.rpm.transaction.TransactionWrapper(base.conf.installroot)
['{}-{}-{}'.format(hdr['name'], hdr['version'], hdr['release'])
for hdr in ts.dbMatch("name", "gpg-pubkey")]
dnf/dnssec/dnsseckeyverification.py
Outdated
def __pkg_name_into_key(pkg): | ||
# type: (str) -> KeyInfo | ||
# Load output of the rpm -qi call | ||
p1 = subprocess.Popen(["rpm", "-qi", pkg], stdout=subprocess.PIPE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here, use rpm library.
Thanks for the review. I added some comments directly to yours. The rest is obvious. |
Flake8 detected 18 issues on b1d88ea |
I rewrote parts of the code where you requested changes. Missing parts are:
|
dnf/dnssec/__init__.py
Outdated
from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\ | ||
Validity | ||
# Functions | ||
from dnf.dnssec.dnsseckeyverification import any_msg, email2location, nice_user_msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[F401] 'dnf.dnssec.dnsseckeyverification.any_msg' imported but unused
dnf/base.py
Outdated
# and therefore there is no way to know for sure (this is mainly for | ||
# backward compatibility) | ||
if self.conf.dns_verification: | ||
if dns_result in (dnssec.Validity.VALID, dnssec.Validity.PROVEN_NONEXISTENCE): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[E501] line too long (102 > 100 characters)
dnf/dnssec/__init__.py
Outdated
import dnf.exceptions | ||
|
||
# Classes | ||
from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[F401] 'dnf.dnssec.dnsseckeyverification.DNSSECKeyVerification' imported but unused
I have a few questions:
|
It creates a list of all keys installed in the RPM database and checks one by one before any transaction. Line 93 in 9015583
dnf/dnf/dnssec/dnsseckeyverification.py Line 203 in 9015583
Yes, this has no information about the key origin. |
@msehnout This is an interesting idea, but the implementation needs to move to libdnf or otherwise half of the package manager stack won't be able to use it. |
@Conan-Kudo, can you please elaborate more on what exact components of the stack won't be able to use it? |
@msehnout PackageKit, RPM-OSTree, and microdnf use libdnf. Stuff only implemented in DNF won't work for those tools. |
@Conan-Kudo, well that is unfortunate. But, I'd go with this implementation for now. We still need to cooperate with Fedora Infra and Copr to make it useful at least in dnf. Support for libdnf and all the tools that require it can be implemented later on. |
Unfortunately, that's not going to be true for much longer. A big part of what's been going on in DNF development has been to move the core logic from Python to C++ to improve performance and consistency in behavior. @m-blaha should be able to help with implementing this in libdnf. |
Still, this PR can be merged and replaced with different implementation later on. |
@m-blaha any update? |
dnf/cli/main.py
Outdated
@@ -42,6 +42,8 @@ | |||
import os.path | |||
import sys | |||
|
|||
import dnf.dnssec.dnsseckeyverification as dnssec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use absolute import:
import dnf.dnssec
dnf/base.py
Outdated
@@ -47,6 +47,7 @@ | |||
import dnf.conf | |||
import dnf.conf.read | |||
import dnf.crypto | |||
import dnf.dnssec as dnssec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use absolute import:
import dnf.dnssec
dnf/dnssec/__init__.py
Outdated
import dnf.exceptions | ||
|
||
# Classes | ||
from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge these into this file and rename it to dnf/dnssec.py
dnf/CMakeLists.txt
Outdated
@@ -10,3 +10,4 @@ ADD_SUBDIRECTORY (module) | |||
ADD_SUBDIRECTORY (rpm) | |||
ADD_SUBDIRECTORY (yum) | |||
ADD_SUBDIRECTORY (db) | |||
ADD_SUBDIRECTORY (dnssec) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed after making other changes
dnf/base.py
Outdated
@@ -2298,14 +2299,33 @@ def _prov_key_data(msg): | |||
logger.info(msg, keyurl, info.short_id) | |||
continue | |||
|
|||
# DNS Extension: create a key object, pass it to the verification class | |||
# and print its result as an advice to the user. | |||
if self.conf.dns_verification: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to gpgkey_dns_verification
dnf/base.py
Outdated
# and therefore there is no way to know for sure (this is mainly for | ||
# backward compatibility) | ||
if self.conf.dns_verification: | ||
if dns_result in (dnssec.Validity.VALID, dnssec.Validity.PROVEN_NONEXISTENCE): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a comment what PROVEN_NONEXISTENCE means
it doesn't make sense for someone who doesn't understand dnssec
dnf/cli/main.py
Outdated
@@ -87,6 +89,8 @@ def _main(base, args, cli_class, option_parser): | |||
|
|||
# our core object for the cli | |||
base._logging._presetup() | |||
# TODO: move somewhere I can check configuration options | |||
dnssec.RpmImportedKeys.check_imported_keys_validity() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to base.py - fill_sack(), probably somewhere under "if load_available_repos:"
we want dnssec checking only if we load remote repositories and work with them
revoking keys makes sense only before downloading and installing something from repos,
having revoked keys on system is ok for remove, local queries, etc.
dnf/dnssec/dnsseckeyverification.py
Outdated
import hashlib | ||
import logging | ||
import re | ||
import unbound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix import ordering: system, newline, 3rd party, newline, project
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also move unbound import to _cache_miss
if unbound import fails with ImportError -> raise RuntimeError explaining that gpgkey_dns_verification requires unbound.
dnf/dnssec/dnsseckeyverification.py
Outdated
.lower() | ||
return digest + "." + tag + "." + domain | ||
else: | ||
raise DnssecError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise DnssecError()
dnf/dnssec/dnsseckeyverification.py
Outdated
:return: | ||
""" | ||
split = email_address.split("@") | ||
if len(split) == 2: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion:
if len(split) != 2:
raise DnssecError()
and then save 4 spaces of indentation
We have decided to wait for requested changes to be made and then we'll merge this in. |
dnf/dnssec.py
Outdated
logger.info(any_msg("GPG Key {} is valid".format(key.email))) | ||
elif result == Validity.PROVEN_NONEXISTENCE: | ||
logger.info(any_msg("GPG Key {} does not support DNS verification" | ||
.format(key.email))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[E128] continuation line under-indented for visual indent
dnf/dnssec.py
Outdated
for key in keys: | ||
result = DNSSECKeyVerification.verify(key) | ||
# TODO: remove revoked keys automatically | ||
#logger.info(any_msg("Key associated with identity " + key.email + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[E265] block comment should start with '# '
dnf/dnssec.py
Outdated
logger.info(any_msg("GPG Key {} has been revoked and should be removed immediately" | ||
.format(key.email))) | ||
else: | ||
logger.info(any_msg("GPG Key {} could not be tested".format(key.email))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[W292] no newline at end of file
dnf/dnssec.py
Outdated
.format(key.email))) | ||
elif result == Validity.REVOKED: | ||
logger.info(any_msg("GPG Key {} has been revoked and should be removed immediately" | ||
.format(key.email))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[E128] continuation line under-indented for visual indent
dnf/dnssec.py
Outdated
logger.info(any_msg("GPG Key {} could not be verified, because DNSSEC signatures are\ | ||
bogus. Possible causes: wrong configuration of the DNS server, \ | ||
MITM attack" | ||
.format(key.email))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Flake8]
[E128] continuation line under-indented for visual indent
I wrote an additional test for copr builds. It is available here: |
📌 Commit bbc78f0 has been approved by |
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
THe Python API for Unbound is required for the DNSSEC extension to work, but it is optional. The second dependency is python2-enum34 which backports support for enumerations into Python2. An enumeration is used for expressing validity in a typed way as opposed to plain integer constants. This type is then used in type hints, which are written in Python2 compatible way (comments under function definitions). Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
💥 Test timed out |
@rh-atomic-bot retry |
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
THe Python API for Unbound is required for the DNSSEC extension to work, but it is optional. The second dependency is python2-enum34 which backports support for enumerations into Python2. An enumeration is used for expressing validity in a typed way as opposed to plain integer constants. This type is then used in type hints, which are written in Python2 compatible way (comments under function definitions). Closes: #1085 Approved by: m-blaha
Closes: #1085 Approved by: m-blaha
💥 Test timed out |
THe Python API for Unbound is required for the DNSSEC extension to work, but it is optional. The second dependency is python2-enum34 which backports support for enumerations into Python2. An enumeration is used for expressing validity in a typed way as opposed to plain integer constants. This type is then used in type hints, which are written in Python2 compatible way (comments under function definitions).
Motivation
Every time a new repository is added with GPG signed packages, dnf asks the user to accept the verification key, but there seems to be very little amount of users who actually do the key verification instead of simply accepting the key.
Of course the official keys are available in the local file system:
and others are downloaded over TLS secured HTTP:
but still, there is no need to do the verification manually.
Unfortunately there is no way of automatic key revocation. GPG keys are stored inside of the RPM database and they simply stay there forever.
Implementation
DNS can be used to store any data even GPG keys as defined in RFC 7929:
https://tools.ietf.org/html/rfc7929
These records are then signed using DNSSEC, thus ensuring authenticity.
dnf can use libundound as an implementation of a validating resolver to obtain the key using DNS and verify it using DNSSEC. This way the verification process will be automatic.
Testing
I am currently using custom VM with a simulation of the DNS hierarchy. I could try to turn it into a Vagrantfile, that would run the testing environment. Is it ok for you or would you like to use a different approach to testing?
Pending issues
This work is based on my master's thesis, but the text is not finished yet. It will be ready in 2-3 weeks.