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

Add second signing mechanism based on native 'openpgp' Go library #198

Closed
wants to merge 6 commits into from

Conversation

mfojtik
Copy link
Contributor

@mfojtik mfojtik commented Jan 4, 2017

Changes made:

  • Refactored signature/ package to deal with 2 mechanism
  • Moved SigningMechanism to types.SigningMechanism (to avoid import loops)
  • Moved default gpgme based signing mechanism to signature/gpgme so it can be opted-out when you don't want to have CGO dependency
  • The tests inside signature/ package now use openpgp package (this might be a follow up to add tests for gpgme as well, however I wanted to make signature/ package clean so it can be imported without relying on CGO)

NOTE that only verification is currently supported via Golang opengpg. We can work on signing using Encode() but as a follow up.

Also note that this is breaking internal API in some ways (you have to now pass the mechanism you want to use to some functions, so plumbing on skopeo side will be required to use gpgme mechanism).

The unit tests are passing.

@mtrmac @rhatdan @smarterclayton @pweil- PTAL

publicKey string
}

cases := []tc{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtrmac I made this test in parity with gpgme test so we can be sure the verification works the same for both.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, a table-driven test is nicer, though it would be nice to keep the comments and ideally to use the same set of tests for both implementations. We can tinker with this after the general structure of the code is fixed, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can add human description as messages in the table test, so when it spits error you can quickly identify the affected case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure, that would be even better.

// Pass a nil pointer to, kind of, test that the return value does not depend on the
// image parameter..
sar, parsedSig, err = pr.isSignatureAuthorAccepted(m, nil, sig)
// FIXME: The error is: errors.InvalidArgumentError("no armored data found") but for
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtrmac I will need some help here :-) I think it still works fine, but for some reason the error is not what you are expecting.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it really no armored data found here? I see InvalidSignatureError{msg:"Invalid GPG signature: (*packet.Signature)(nil)"} .

And that, in turn, happens because the public key is unknown, i.e. MessageDetails.SignedByKeyId is set but MessageDetails.SignedBy is nil because we don’t have the key; and when SignedBy is nil, openpgp.readSignedMessage does not even set up a signatureCheckReader, so Signature is never set to non-nil.

And, that ultimately gets us an InvalidSignatureError, not a PolicyRequirementError—but then note that this test case does not require a PolicyRequirementError. So that seems to be perfectly fine as far as this test is concerned at least; though the Invalid GPG signature: (*packet.Signature)(nil) error message could be improved, as could the gpgme handling at this level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks! this trace is really helpful

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

As a quick first pass, we really need to figure out a different way to customize the mechanism so that prSignedBy can still respect pr.KeyType.

if err != nil {
return sarRejected, nil, err
}
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

Moving the mechanism selection/creation out of prSignedBy really won’t work:

  • The mechanism in principle depends on pr.KeyType; a single PolicyContext.IsRunningImageAllowed may involve policies with various prSignedBy instances with different pr.KeyType values. Right now we only support SBKeyTypeGPGKeys but the code above already anticipates other values.

  • With keys specified inside the policy as pr.KeyPath/pr.KeyData, we really want a temporary instance of the mechanism with its own state; the caller shouldn’t have to deal with creating individual temporary directories.

AFAICT the easiest way to do this is to have the two SigningMechanism implementations available in a single package, chosen by a build tag. Then nobody else needs to care.

Another possibility would be to use types.SystemContext, as we do for other environment overrides (and signature.NewPolicy.Context(ctx *types.SystemContext, policy *Policy) copying the ctx reference into the returned PolicyContext); but types.SystemContext is designed to always have a default, so we would need to build a reference to one of the implementations into signature anyway… and then we are more or less back in the build tag area.

@runcom is there another approach I am missing?

(I’d be happy to work on this, but I do need you to ACK that the chosen approach is usable in OpenShift.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

@mfojtik @runcom Experimentally demonstrating what using build tags would look like in #207 (DO NOT MERGE right now). Would that work for you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtrmac the build tags are the "last resort" to me as the management of them proved cumbersome in openshift (we forgot to add it for registry that resulted in storage driver disabled in release, etc..) They also need to be documented as they are exposed to downstream (origin).

The cleanest (in go way) solution would be to separate the packages and have interface for them. The interface (SigningMechanism) is not really a good interface if an implementation is used instead anyway of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The choice of "default" mechanism should be on the user of this library (skopeo vs. origin). We should have specific tests that exercise both implementation properly locked in their packages.

I picked openpgp as a default for signature/ package tests as it does not require any CGO dependency. I'm fine moving some of the test to be package specific as a follow up (even if that means we duplicate tests or create some framework for tests so you can exchange the signing mechanism to prove both implementation produce the same results..)

Copy link
Collaborator

Choose a reason for hiding this comment

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

the build tags are the "last resort" to me as the management of them proved cumbersome in openshift (we forgot to add it for registry that resulted in storage driver disabled in release, etc..)

This seems like a kind of thing which could be once for all ensured using a build-time test.

The cleanest (in go way) solution would be to separate the packages and have interface for them.

Sure, but something somewhere has to make a choice, and forcing callers who do not care (they may not even know that cryptography is involved somewhere in the stack) to make such a choice is not really clean either. (In the worst case, in the future, we could have a caller who provides a X.509 signature, knows that, and is still forced to make a choice between OpenPGP implementations.)

The interface (SigningMechanism) is not really a good interface if an implementation is used instead anyway of it.

Look at #207 - the interface stays, and really is not the point; the point is what and how chooses the which implementation is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtrmac i think I'm fine with build tags for now although I will prefer the containers_image_gpgme to be the tag (in that way we don't have to add build tag to all build scripts in origin to make sure we don't build anything that requires openpgp).

We have to talk how I'm going to use this library in Origin as I think we are still on the same page (we just going to verify the signature, not policy.json, so probably we will just use the signature initially)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtrmac + before I forget, we also need some way to extract the metadata (critical/optional) without us parsing the JSON content (which means that the JSON content should be somehow formalized or have helper function in this library).

m.ctx.keyring = append(m.ctx.keyring, entity)
}
if len(m.ctx.keyring) == 0 {
return nil, errors.New("no public keys found")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test is imprecise; if there already have been keys imported before, this won’t notice.

Copy link
Collaborator

Choose a reason for hiding this comment

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

… but then see the other conversations about importing 0 keys possibly succeeding… it’s not entirely obvious what needs changing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree, this check is necessary.

however I have question about this function... is the intention to always append to keyring when this function is called or should I reset the keyring? What if I have duplicate keys?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Strictly speaking, undefined :)

Right now this is not intended as a generic abstraction over key management systems (import/export/list keys etc.), so that someone could build a key management CLI on top. SigningMechanism is really here only to sign and verify.

The only place where ImportKeysFromBytes is called in production is prSignedBy, which creates a completely separate GPG home directory in the gpgme implementation: the idea is that, regardless of anything in $HOME or $GNUPGHOME, prSignedBy uses only the keys specified in the prSignedBy object.

So, with gpgme, keys are stored in files and keys need to be imported into the keyring before being used; therefore, ImportKeysFromBytes has a persistent effect (adds keys to $optionalDir or so, and they will persist for future uses of that directory); prSignedBy creates a private directory, imports keys exactly once, uses that directory for one (or more, in the future) verifications.

With openpgp, we can use in-memory keys; so, there is no need for ImportKeysFromBytes to have a persistent effect; prSignedBy can create an in-memory object (TODO we should skip creating the directory in that case, and move that into the SigningMechanism as well), import keys into it, and use it for validation.

As it happens, ImportKeysFromBytes is always called only once per directory / object / (in the future) context in prSignedBy, so it does not matter whether the keyring is reset, hence the “strictly speaking, undefined”.

But, per naming of the function, it seems more reasonable to append keys than to override; it is Import, not OverrideWithImport after all. And if SigningMechanism ever grew into the export/import/list key management abstraction, a non-destructive import seems much more useful to me.

As for duplicate keys (note that they may be duplicate both between old keyring contents and the imported data, and within just the imported data), as long as signatures validate using the imported keys and only the imported keys, we don’t really care right now (well, other than one somewhat misguided test in mechanism_test.go, but we can change that one).

Eventually, to implement SBKeyTypeSignedByGPGKeys, if that ever happens, we will need to care about signatures attached to public keys, and the ability to import new signatures to already existing public keys, and this will become more complex. But that’s not a constraint right now.

return nil, errors.New("no public keys found")
}
keyIdentities := []string{}
for _, entity := range m.ctx.keyring {
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are identities of all keys in the keyring, not just those imported right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see the question above.


func (m OpenPGPMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
if len(m.ctx.keyring) == 0 {
return nil, "", errors.New("no public keys imported")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this explicit check necessary? Admittedly it results in a nice error message, but it seems to me that “public key with fingerprint XXX not found” should happen anyway?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree, removed.

if len(m.ctx.keyring) == 0 {
return nil, "", errors.New("no public keys imported")
}
md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.ctx.keyring, nil, nil)
Copy link
Collaborator

Choose a reason for hiding this comment

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

(Note to self: I didn’t review this in detail yet.)

// FIXME: The error is: errors.InvalidArgumentError("no armored data found") but for
// some reason it is surfaced here and not translated to PolicyRequirementError.
// However the sar is rejected...
assertSARRejectedPolicyRequirement(t, sar, parsedSig, PolicyRequirementError(""))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, gpgme happens to accept empty bytes (the []byte{} in NewPRSignedByKeyData) as valid input, “correctly importing” it as a set of zero keys. (It also happens to accept completely invalid input, but that’s beside the point).

With gpgme, the code path fails in the if len(trustedIdentities) == 0 { case, which is a PolicyRequirementError; with openpgp, it fails in the … err := mechanism.ImportKeysFromBytes(); if err != nil case, which is an operation error (like ioutil.ReadFile failing). Though arguably the len(trustedIdentities) == 0 is an invalid PolicyRequirement, not a failure to fulfill it… it really isn’t that well defined.

I guess, considering https://tools.ietf.org/html/rfc4880#section-3.6 , it could be argued that an empty keyring is invalid… We will need to nail these things down.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Though arguably the len(trustedIdentities) == 0 is an invalid PolicyRequirement, not a failure to fulfill it… it really isn’t that well defined.

Looking at other cases, that should indeed be a PolicyRequirementError. What to do on corrupt keyring data is unclear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed both.

// Pass a nil pointer to, kind of, test that the return value does not depend on the
// image parameter..
sar, parsedSig, err = pr.isSignatureAuthorAccepted(m, nil, sig)
// FIXME: The error is: errors.InvalidArgumentError("no armored data found") but for
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it really no armored data found here? I see InvalidSignatureError{msg:"Invalid GPG signature: (*packet.Signature)(nil)"} .

And that, in turn, happens because the public key is unknown, i.e. MessageDetails.SignedByKeyId is set but MessageDetails.SignedBy is nil because we don’t have the key; and when SignedBy is nil, openpgp.readSignedMessage does not even set up a signatureCheckReader, so Signature is never set to non-nil.

And, that ultimately gets us an InvalidSignatureError, not a PolicyRequirementError—but then note that this test case does not require a PolicyRequirementError. So that seems to be perfectly fine as far as this test is concerned at least; though the Invalid GPG signature: (*packet.Signature)(nil) error message could be improved, as could the gpgme handling at this level.

}

func (m OpenPGPMechanism) ImportKeysFromBytes(blob []byte) ([]string, error) {
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(blob))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this should also accept unarmored data; with keyData inside policy.json containing a base64-encoded representation of keyData, it would be rather silly, and wasting space, to have a base64-encoded of a base64-based armor encoding of the underlying key.

Copy link
Collaborator

Choose a reason for hiding this comment

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

(These kinds of things have so far not been really defined because “whatever GPG does” has worked for us; we will need to gradually nail them down now.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can detect if the blob start with --- BEGIN PGP use ReadKeyRing() if data are not armored.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Considering https://github.com/golang/crypto/blob/master/openpgp/armor/armor.go#L165 , such a trivial check may not be sufficient (PGP implementations have frequently been fairly lenient about leading text before armor, e.g. handling an e-mail message with full headers), though it may affect very few cases in practice.

It is, of course, easy enough, to call both ReadKeyRing and ReadArmoredKeyRing, in case either succeeds; if both fail, returning a really good error message may be difficult.

@mfojtik
Copy link
Contributor Author

mfojtik commented Jan 10, 2017

@mtrmac I'm fine closing this PR in favor of #11

@mfojtik mfojtik closed this Jan 10, 2017
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

2 participants