Skip to content
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

Merged
merged 6 commits into from Oct 3, 2018

Conversation

msehnout
Copy link
Contributor

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.

Importing GPG key 0x46CD093F:
 Userid     : "msehnout_neovim (None) <msehnout#neovim@copr.fedora.org>"
 Fingerprint: 5E84 DA77 A5C0 121E D09C 60B9 4603 C1E8 46CD 093F
 From       : https://copr-be.cloud.fedora.org/msehnout/neovim/pubkey.gpg
Is this ok [y/N]: y
Key imported successfully

Of course the official keys are available in the local file system:

gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch

and others are downloaded over TLS secured HTTP:

gpgkey=https://dl.google.com/linux/linux_signing_key.pub

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

  • libunbound configuration file
  • testing of backwards compatibility (some domains might not have correct DNSSEC setup)

This work is based on my master's thesis, but the text is not finished yet. It will be ready in 2-3 weeks.

@m-blaha m-blaha self-assigned this May 30, 2018
dnf/base.py Outdated
@@ -37,6 +37,7 @@
import dnf.conf
import dnf.conf.read
import dnf.crypto
import dnf.dnssec.dnsseckeyverification as dnssec
Copy link
Member

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:
Copy link
Member

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."))
Copy link
Member

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)
Copy link
Member

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.

return digest + "." + tag + "." + domain
else:
# Error
return "Error"
Copy link
Member

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.

# type: Dict[str, Union[str, NoKey]]

@staticmethod
def __cache_hit(key_union, input_key_string):
Copy link
Member

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.

@staticmethod
def __cache_miss(input_key):
# type: (KeyInfo) -> Validity
RR_TYPE_OPENPGPKEY = 61
Copy link
Member

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

RR_TYPE_OPENPGPKEY = 61
ctx = unbound.ub_ctx()
# TODO: is this the right place to put this file?
ctx.config("/etc/dnf/libunbound.conf")
Copy link
Member

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.

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')]
Copy link
Member

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")]

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)
Copy link
Member

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.

@msehnout
Copy link
Contributor Author

Thanks for the review. I added some comments directly to yours. The rest is obvious.

@dnf-bot
Copy link
Member

dnf-bot commented Jul 2, 2018

Flake8 detected 18 issues on b1d88ea
Visit https://sider.review/gh/3671909/pull_requests/1085 to review the issues.

@msehnout
Copy link
Contributor Author

msehnout commented Jul 2, 2018

I rewrote parts of the code where you requested changes. Missing parts are:

from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\
Validity
# Functions
from dnf.dnssec.dnsseckeyverification import any_msg, email2location, nice_user_msg
Copy link
Member

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):
Copy link
Member

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)

import dnf.exceptions

# Classes
from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\
Copy link
Member

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

@tyll
Copy link

tyll commented Aug 15, 2018

I have a few questions:

  • How does this handle key revocation?
  • Will this be used to verify keys from the local file system/downloaded via HTTPS additionally via DNS?

@msehnout
Copy link
Contributor Author

How does this handle key revocation?

It creates a list of all keys installed in the RPM database and checks one by one before any transaction.

dnssec.RpmImportedKeys.check_imported_keys_validity()

class RpmImportedKeys:

Will this be used to verify keys from the local file system/downloaded via HTTPS additionally via DNS?

Yes, this has no information about the key origin.

@Conan-Kudo
Copy link
Member

@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.

@msehnout
Copy link
Contributor Author

@Conan-Kudo, can you please elaborate more on what exact components of the stack won't be able to use it?

@Conan-Kudo
Copy link
Member

@msehnout PackageKit, RPM-OSTree, and microdnf use libdnf. Stuff only implemented in DNF won't work for those tools.

@msehnout
Copy link
Contributor Author

@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.

@Conan-Kudo
Copy link
Member

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.

@msehnout
Copy link
Contributor Author

Still, this PR can be merged and replaced with different implementation later on.

@msehnout
Copy link
Contributor Author

@m-blaha any update?

dnf/cli/main.py Outdated
@@ -42,6 +42,8 @@
import os.path
import sys

import dnf.dnssec.dnsseckeyverification as dnssec
Copy link

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
Copy link

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

import dnf.exceptions

# Classes
from dnf.dnssec.dnsseckeyverification import DNSSECKeyVerification, KeyInfo, RpmImportedKeys,\
Copy link

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

@@ -10,3 +10,4 @@ ADD_SUBDIRECTORY (module)
ADD_SUBDIRECTORY (rpm)
ADD_SUBDIRECTORY (yum)
ADD_SUBDIRECTORY (db)
ADD_SUBDIRECTORY (dnssec)
Copy link

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:
Copy link

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):
Copy link

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()
Copy link

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.

import hashlib
import logging
import re
import unbound
Copy link

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

Copy link

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.

.lower()
return digest + "." + tag + "." + domain
else:
raise DnssecError
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise DnssecError()

:return:
"""
split = email_address.split("@")
if len(split) == 2:
Copy link

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

@dmach
Copy link

dmach commented Sep 19, 2018

We have decided to wait for requested changes to be made and then we'll merge this in.
It's good enough to be tested by dnf users.
Later on, we'll move the code into libdnf.

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)))
Copy link
Member

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 +
Copy link
Member

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)))
Copy link
Member

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)))
Copy link
Member

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)))
Copy link
Member

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 Show resolved Hide resolved
@msehnout
Copy link
Contributor Author

I wrote an additional test for copr builds. It is available here:
https://github.com/msehnout/dnf-testing/blob/master/tests/happy-path-from-copr-build.sh

@m-blaha
Copy link
Member

m-blaha commented Sep 25, 2018

@rh-atomic-bot r+

@rh-atomic-bot
Copy link

📌 Commit bbc78f0 has been approved by m-blaha

@rh-atomic-bot
Copy link

⌛ Testing commit bbc78f0 with merge 04c11bf...

rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
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
rh-atomic-bot pushed a commit that referenced this pull request Sep 25, 2018
@rh-atomic-bot
Copy link

💥 Test timed out

@m-blaha
Copy link
Member

m-blaha commented Sep 27, 2018

@rh-atomic-bot retry

@rh-atomic-bot
Copy link

⌛ Testing commit bbc78f0 with merge b4a91a2...

rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
Closes: #1085
Approved by: m-blaha
rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
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
rh-atomic-bot pushed a commit that referenced this pull request Sep 27, 2018
@rh-atomic-bot
Copy link

💥 Test timed out

Martin Sehnoutka added 6 commits October 2, 2018 15:07
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).
@m-blaha m-blaha merged commit c90c00c into rpm-software-management:master Oct 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants