-
Notifications
You must be signed in to change notification settings - Fork 395
Add support for validating signatures from PGP subkeys #2874
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 support for validating signatures from PGP subkeys #2874
Conversation
77893fc to
f79e1b8
Compare
mtrmac
left a comment
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.
Thanks!
containers/podman#16406 contains a long discussion about expired (or revoked) keys, IIRC it’s fairly unclear what is the right thing to do for long-term artifacts like software, and with no trusted timestamps.
Adding what, 150? new lines of code to the security critical signing code path is also a bit unattractive (although that’s, by itself, not at all a reason not to do the right thing).
There’s also a bit of awkward timing in that #2569 will probably replace the default / recommended verification implementation.
OTOH, these design concerns are, right now, also a topic there: #2569 (comment) .
I noticed this as I was finishing out the implementation but based on my understanding of the comments. It appears the GPGPME/Go OpenPGP will still exist, it just won't be the hot path. As the sequoia OpenPGP implementation has also been something that's still being worked though, having any support for subkeys IMO is ideal.
To my understanding, once a key is revoked or expired the signature is no longer valid. Regardless of when it was signed. This default to close IMO is a feature. Revocations are packets within the key just like anything else and generally the systems/users doing validation of OpenPGP signatures understand the implications of this. If a key is revoked and pushed to a keyserver, the user/machine has to pull down the updated packets for it to take effect within the verification pipeline. If you are aware that a revocation has been published but you still need to use a previously signed image it would probably be more ideal for you to mirror the image and sign it yourself than the much less secure option of not importing the updated key. This same problem exists is most signing schemes (key rotation). |
|
Note to self: Sequoia, by default, generates keys where the signing key is a subkey, so we almost certainly need some version of this. |
|
I've been meaning to rebase and fix this now that a whole new set of keyrings exist. Happy to do so |
|
I think it would be fair for me to first actually read this PR at least once :) |
f79e1b8 to
02743dc
Compare
Signed-off-by: Danny Grove <[email protected]>
This change should update the gpgme and openpgp implmenetations to support validating signatures made by signing subkeys. It will close an open issue that was referenced specifically in [podman](containers/podman#16406), but should resolve this across a number of tools that use this library. Signed-off-by: Danny Grove <[email protected]>
7ef722b to
7bedb9b
Compare
Signed-off-by: Danny Grove <[email protected]>
Signed-off-by: Danny Grove <[email protected]>
7bedb9b to
bd6e15e
Compare
|
@mtrmac everything has been rebased and 2 keyrings have been added. One for the subkey validation and 1 for policy checking around revoked subkeys after a signature is made. As a sidenote, it might be easier longer term to export the secret keys to file and rebuild the keyring on the fly. Then adding/removing or replacing keys becomes a more atomic process |
mtrmac
left a comment
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.
Thanks!
Getting back to this now:
The design of verifyAndExtractSignature / signatureAcceptanceRules is that it centralizes all of the “is a signature valid“ policy for simple signatures. There should be no policy anywhere outside that function, other than what is defined as explicit parameters to that function. In particular, no two code paths should differ of what is permitted / rejected (other than explicit variable parameters like “which public key is trusted”).
So, with this PR, we end up with three code paths:
VerifyImageManifestSignatureUsingKeyIdentityListjust callingverifyAndExtractSignature(with primary-key-only matching)VerifyDockerManifestSignatureimposing a subkey restriction in advance, and then callingverifyAndExtractSignature(with the old-style primary-key-only matching still happening)prSignedBy.isSignatureAuthorAcceptedusingverifyAndExtractSignature, and taking subkeys into account insignatureAcceptanceRules
That’s not acceptable.
But, also, verifyAndExtractSignature is private and we have full freedom to redesign it as necessary.
Eventually, there should be unit test code coverage of ~every reasonably reachable line in ./signature. But we can deal with that after the structure settles.
| // using mech. | ||
| func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte, | ||
| expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) { | ||
| _, signerKeyIdentity, err := mech.Verify(unverifiedSignature) |
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 don’t understand the location (and to an extent, purpose) of this.
VerifyImageManifestSignatureUsingKeyIdentityListis a public API. It must also handle the situation correctly.- This does an extra cryptographic verification, an unwanted overhead
- … and AFAICS it doesn’t affect
expectedKeyIdentity, so a subkey signature would still fail in this function.
|
|
||
| // Get the primary key fingerprint | ||
| primarySubkey := key.SubKeys() | ||
| if primarySubkey != nil && primarySubkey.Fingerprint() == expectedKeyIdentity { |
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 don’t know whether we should support setting expectedKeyIdentity to a subkey at all; but if we would accept that, then it doesn’t make sense to me to accept any other key than that particular subkey.
Also, this functionality directly contradicts the function name. Such a confusion in security-critical code is a risk in itself.
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.
Generally you'd use !<subkey fingerprint> though I don't believe the current container-polices.d spec makes any specific note of that. But since that is a common to gpg signing implementation not supporting it may be labeled as a bug.
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.
For me, the primary thing that matters is the prSignedBy usage pattern, where newEphemeralGPGSigningMechanism is used with raw public keys, returns $identifiers, and then, while verifying signatures, we sanity-check that the signatures not only are valid in general, but match $identifiers.
(One could argue that the sanity-check is entirely unnecessary, I suppose.)
The way the various implementations of newEphemeralGPGSigningMechanism import keys, $identifiers have always been fingerprints of the primary keys; so that’s the situation I most care about.
Non-prSignedBy callers of verification should get consistent results with prSignedBy, but I don’t worry much about cases where they might try to do something impossible to do via prSignedBy.
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.
current container-polices.d spec
? Google finds nothing.
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.
Apologies it's container-policy.json(5)
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.
policy.json never had a feature to specify fingerprints of any kinds — users would point at whole keyrings.
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.
Perfect then we at least don't have to worry about that piece. If an end-user only wanted to support signatures from a specific subkey they can generate the keyring they specifically care about.
| } | ||
|
|
||
| // isKeyOrValidSubkey checks if the signerKeyIdentity is either the same as the expectedKeyIdentity, or is a valid | ||
| // subkey of the expectedKeyIdentity |
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 can’t see anything dealing with revocation / expiration, so what does “valid” mean here? (We very likely don’t need to explicitly handle revocation / expiration, but comments need to be clear about what the functions do.)
| } | ||
|
|
||
| // For gpgme mechanism, check subkey relationships | ||
| if gpgmeMech, ok := mech.(*gpgmeSigningMechanism); ok { |
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.
Having such a function copy&pasted for every transport is fragile and hard to follow. Centralizing the conditional support via an optional interface (perhaps something like signingMechanismWithPassphrase, though even the method could be private), and handling the condition in a single caller, would make this more explicit.
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 wasn't sure of the impact of centralizing that. I see at the top of the file there are flags for gpgme vs. openpgp. This is a part of go that I am not familiar with so I erred on the side of separation based on the mechanism used since it seemed like the most straight-forward and easy to reason about approach
| for _, entity := range m.keyring { | ||
| // Check if this entity's primary key matches the expected fingerprint | ||
| if entity.PrimaryKey != nil { | ||
| primaryFingerprint := strings.ToLower(fmt.Sprintf("%x", entity.PrimaryKey.Fingerprint)) |
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.
%x is already lower-case.
| } | ||
| } | ||
|
|
||
| // Case 3: The expected key is a subkey, and the signer is the primary key of the same entity |
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.
Similarly to the GPG case, with expectedFingerprint matching a subkey, ignoring that and accepting any other subkey is surprising.
mtrmac
left a comment
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.
Looking at the possible implementation approaches:
… Sequoia in #2876 is already implemented to return the primary key’s fingerprint as an outcome of verification.
| } | ||
|
|
||
| // isSubkeyOf checks if signerKeyIdentity is a subkey of expectedKeyIdentity | ||
| func (m *gpgmeSigningMechanism) isSubkeyOf(signerKeyIdentity, expectedKeyIdentity string) (bool, 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.
Reading the GPGME implementation, yes, we’ll probably need to issue a GetKey of some kind to be able to check these things.
| } | ||
|
|
||
| // isSubkeyOf checks if signerKeyIdentity is a subkey of expectedKeyIdentity | ||
| func (m *openpgpSigningMechanism) isSubkeyOf(signerKeyIdentity, expectedKeyIdentity string) (bool, 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.
It seems that we can immediately read md.SignedBy.Entity.PrimaryKey.Fingerprint to get the primary key. I didn’t yet verify that it is always available.
|
@drGrove I think the structure in #2905 might be an alternative approach:
|
I think at least for the primary success smoke cases, having a test work with “the expected use case” is valuable: If users have a long-term key stored in a Admittedly, using a There’s also the history of #2875 — it would have been nice, at that point, to have a script that regenerates everything, not just the keyrings, but also the public keys and signatures. In that sense, rebuilding keyrings but still using manually-built keys would be a halfway step. But then again, hopefully we won’t need to regenerate keys again, for a long time. |
That doesn't seem to align with supporting pinned signing subkeys then |
|
Hum, not just accepting signatures by subkeys, but specifically restricting the policy to only accept a specified subkey (if that’s what you mean) would be yet another separate feature — notably it would need a new config option, I think. |
… a subkey signature This matches Sequoia, and allows accepting subkey signatures via signatureAcceptanceRules. FIXME: Needs tests - for keyIdentityForVerificationKeyIdentity - replace keys, generate v3 signatures. Currently the new keys+sigs come from containers#2874, credit to @drGrove Signed-off-by: Miloslav Trmač <[email protected]>
|
#2905 was merged. Thanks for this work! |
This change should update the gpgme and openpgp implmenetations to support validating signatures made by signing subkeys. It will close an open issue that was referenced specifically in podman, but should resolve this across a number of tools that use this library.