Implement basic form of `snapcraft register-key` #726

Merged
merged 22 commits into from Sep 8, 2016

Conversation

Projects
None yet
5 participants
Contributor

cjwatson commented Aug 12, 2016

This adds a basic register-key command that can be used to register keys
from your snap keyring.

We don't yet have all the prescribed error modes in place, and nor are
we able to tell which keys are already enabled. That requires some more
work on the store side.

Can one of the admins verify this patch?

Can one of the admins verify this patch?

Contributor

cjwatson commented Aug 12, 2016

Note that https://code.launchpad.net/~cjwatson/snap-assertions-service/public-key-headers-endpoint/+merge/302612 and https://code.launchpad.net/~cjwatson/software-center-agent/less-terrible-account-key/+merge/302789 need to be deployed before we can go ahead with this, but it should be possible to review this in the meantime to speed things up.

Collaborator

sergiusens commented Aug 16, 2016

Seems travis needs some deps installed

  Running setup.py install for pygpgme ... error
    Complete output from command /usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-c3o2kata/pygpgme/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-iwyk7pry-record/install-record.txt --single-version-externally-managed --compile:
    running install
    running build
    running build_py
    creating build
    creating build/lib.linux-x86_64-3.5
    creating build/lib.linux-x86_64-3.5/gpgme
    copying gpgme/editutil.py -> build/lib.linux-x86_64-3.5/gpgme
    copying gpgme/__init__.py -> build/lib.linux-x86_64-3.5/gpgme
    running build_ext
    building 'gpgme._gpgme' extension
    creating build/temp.linux-x86_64-3.5
    creating build/temp.linux-x86_64-3.5/src
    x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/usr/include/python3.5m -c src/gpgme.c -o build/temp.linux-x86_64-3.5/src/gpgme.o
    In file included from src/gpgme.c:22:0:
    src/pygpgme.h:24:19: fatal error: gpgme.h: No such file or directory
    compilation terminated.
    error: command 'x86_64-linux-gnu-gcc' failed with exit status 1
Contributor

cjwatson commented Aug 16, 2016

Thanks. I'll fix that in a bit - the store interface is moving quite quickly here so I want to get that settled down first.

Collaborator

sergiusens commented Aug 16, 2016

El 16/08/16 a las 13:10, Colin Watson escribió:

Thanks. I'll fix that in a bit - the store interface is moving quite
quickly here so I want to get that settled down first.

Indeed, not a stranger to that story :-)

snapcraft/_store.py
@@ -73,6 +75,84 @@ def logout():
logger.info('Credentials cleared.')
+def _get_usable_secret_keys(query=None):
+ ctx = gpgme.Context()
@elopio

elopio Aug 17, 2016

Member

we usually try to avoid variable names like this. Specially for somebody like me who doesn't speak great English, ctx is less readable than context.

snapcraft/_store.py
+def _get_usable_secret_keys(query=None):
+ ctx = gpgme.Context()
+ for key in ctx.keylist(query, True):
+ if not key.subkeys or not key.uids or key.disabled or not key.can_sign:
@elopio

elopio Aug 17, 2016

Member

could you group these four conditions in a function with a nice name explaining what this check is about?
like if not _is_usable_key(key), and then in _is_usable_key a comment about what makes a key usable.

snapcraft/_store.py
+ continue
+ mainkey = key.subkeys[0]
+ if (mainkey.expired or mainkey.revoked or mainkey.disabled or
+ mainkey.invalid):
@elopio

elopio Aug 17, 2016

Member

Here too, it would be nice to group the statement in a function. _is_mainkey_usable? I'm not sure about the name.

snapcraft/_store.py
+ mainkey.invalid):
+ continue
+ # We're currently only interested in RSA >= 4096-bit.
+ if mainkey.pubkey_algo != gpgme.PK_RSA or mainkey.length < 4096:
@elopio

elopio Aug 17, 2016

Member

I would move this to a function too, but I tend to write way too many functions :p I won't complain about this one, just saying...

snapcraft/_store.py
+ yield key
+
+
+def _display_fingerprint(fingerprint):
@elopio

elopio Aug 17, 2016

Member

Maybe display is not the right word here, because it's not displaying anything. What about _get_formatted_fingerprint or _format_fingerprint ?

snapcraft/_store.py
+
+
+def _display_fingerprint(fingerprint):
+ assert len(fingerprint) == 40
@elopio

elopio Aug 17, 2016

Member

I don't like assertions. I think it's better to make an if and raise the exception explicitly. The "assert" statement can be disabled and the "if" can't, but that's not likely to happen. My main problem with assertions is that they are intended to discover errors from the programmer, things that should never happen. And for me, that's what unit tests are for. So if you already have a test that makes sure this condition can't happen, the statement is not useful. And if this can happen during the normal operation of the program, that's what exceptions are for, and they are clearer.

snapcraft/_store.py
+}
+
+
+def _display_key(key):
@elopio

elopio Aug 17, 2016

Member

Here again, display is not the right verb. get or format seem better.

snapcraft/_store.py
+ 'You have no usable GPG secret keys matching "{}".'.format(query))
+ key = _select_key(keys)
+ fingerprint = key.subkeys[0].fpr
+ ctx = gpgme.Context()
@elopio

elopio Aug 17, 2016

Member

s/ctx/context
On the line above I was wondering what fpr was, then I saw you assigned it to "fingerprint", that's lovely :D

snapcraft/_store.py
+ store = storeapi.StoreClient()
+ try:
+ store.register_key(key_data.getvalue())
+ except storeapi.errors.InvalidCredentialsError:
@elopio

elopio Aug 17, 2016

Member

@sergiusens this feels like a decorator, right? We are duplicating it in many places, we could just tag the functions that require authentication instead.

@sergiusens

sergiusens Aug 17, 2016

Collaborator

El 16/08/16 a las 23:01, Leo Arias escribió:

In snapcraft/_store.py
#726 (comment):

+def register_key(query):

  • keys = list(_get_usable_secret_keys(query=query))
  • if not keys:
  •    raise RuntimeError(
    
  •        'You have no usable GPG secret keys matching "{}".'.format(query))
    
  • key = _select_key(keys)
  • fingerprint = key.subkeys[0].fpr
  • ctx = gpgme.Context()
  • key_data = io.BytesIO()
  • ctx.export(fingerprint.encode('ascii'), key_data)
  • logger.info('Registering GPG key ...')
  • store = storeapi.StoreClient()
  • try:
  •    store.register_key(key_data.getvalue())
    
  • except storeapi.errors.InvalidCredentialsError:

@sergiusens https://github.com/sergiusens this feels like a decorator,
right? We are duplicating it in many places, we could just tag the
functions that require authentication instead.

Maybe, let's look into that after this lands.

snapcraft/_store.py
+ raise
+ logger.info(
+ 'Done. The GPG key {} will be expected for signing your '
+ 'assertions.'.format(_display_fingerprint(fingerprint)))
@elopio

elopio Aug 17, 2016

Member

"expected" sounds confusing to me. What do you think about: "The GPG key ... will be used to sign your assertions"? I'm not sure it's better though.

Member

elopio commented Aug 17, 2016

This is great, thanks for sharing this @cjwatson !

How hard would it be to write an integration test for the happy path? We are using pyexpect, which makes it easy to handle the input. But I'm wondering about creating the gpg key to be selected. We would have to patch the ~/.gnupg path to use a temp file and maybe we can insert a fake key in there.

cjwatson added some commits Aug 12, 2016

Implement basic form of `snapcraft register-key`
This adds a basic register-key command that can be used to register keys
from your GPG keyring.  At present, they must already be associated with
your Launchpad account, although after the work to move this to SSO is
completed we'll be able to add such associations inline in snapcraft as
well.

We don't yet have all the prescribed error modes in place, and nor are
we able to tell which keys are already enabled.  That requires some more
work on the store side.

LP: #1612730
Address most review feedback
Break up some functions; clarify some names; turn an assertion into a
normal exception.
Use a dedicated macaroon for key registration
The store now requires the modify_account_key ACL for this, so we ask
the user to log in specifically for the register-key operation and fetch
a single-use macaroon.
Refactor on top of snap key management interfaces
We no longer use your normal GPG keyring, and it is no longer necessary
for keys to be associated with your Launchpad account first.
Contributor

cjwatson commented Sep 2, 2016

I've refactored this to use the new snap key management interfaces (https://docs.google.com/document/d/1Bkn9sLgXqkGsgV1eBWO935z5jZJiJQCL24muqw2zD7U). I know that Travis is currently failing; I need to get it to install snapd, but I need to wait for a new enough version to land in xenial-updates first.

At least a basic integration test should be feasible, and I'm working on that now.

cjwatson added some commits Sep 2, 2016

cjwatson added some commits Sep 3, 2016

Mock is_package_installed for register-key tests
Running unit tests no longer requires snapd.
Fix snapd Suggests versioning
xenial-proposed currently has 2.14.2~16.04.
Limit register-key integration test to fake store
This is temporary, but until we have a way to disable the keys again,
we'd only be able to register the test keys once ever.
register-key takes a key name rather than a query
Now that we do exact matching, it doesn't make so much sense to describe
this as a query.
Contributor

cjwatson commented Sep 7, 2016

retest this please

Contributor

josepht commented Sep 7, 2016

Colin said 'retest this please'

Collaborator

sergiusens commented Sep 7, 2016

ok to test

+ if os.getenv('TEST_STORE', 'fake') != 'fake':
+ self.skipTest(
+ 'Cannot register test keys against staging/production until '
+ 'we have a way to delete them again.')
@elopio

elopio Sep 7, 2016

Member

is there a bug to track this?

@cjwatson

cjwatson Sep 8, 2016

Contributor

I've filed https://bugs.launchpad.net/bugs/1621441 and added a reference here.

Member

elopio commented Sep 7, 2016

When I run register-key without a key, I get:

./bin/snapcraft register-key
'NoneType' object is not iterable

snapcraft/storeapi/errors.py
+ fmt = 'Error fetching account information from store: {error}'
+
+ def __init__(self, response):
+ error = '%d %s' % (response.status_code, response.reason)
@elopio

elopio Sep 7, 2016

Member

can you please use format instead?

snapcraft/storeapi/errors.py
+ fmt = 'Key registration failed: {error}'
+
+ def __init__(self, response):
+ error = '%d %s' % (response.status_code, response.reason)
@elopio

elopio Sep 7, 2016

Member

please use format.

cjwatson added some commits Sep 8, 2016

Contributor

cjwatson commented Sep 8, 2016

The register-key 'NoneType' object is not iterable error is really a snapd bug, and I've sent a fix for that as snapcore/snapd#1869. I've also made a change on this side to tolerate the peculiar output.

cjwatson added some commits Sep 8, 2016

Use plain table formatting for key listings
Per discussion on the UX spec.

@sergiusens sergiusens merged commit 35c7cb2 into snapcore:master Sep 8, 2016

3 of 4 checks passed

autopkgtest snaps
Details
autopkgtest integration Success
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
coverage/coveralls Coverage increased (+0.02%) to 97.921%
Details

kalikiana pushed a commit to kalikiana/snapcraft that referenced this pull request Apr 6, 2017

Implement basic form of `snapcraft register-key` (#726)
This adds a basic register-key command that can be used to register
created with `snap`.

We don't yet have all the prescribed error modes in place, and nor are
we able to tell which keys are already enabled.  That requires some more
work on the store side.

LP: #1612730
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment