-
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
Changes from all commits
1f7ff52
7280913
4161631
bd6e15e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| /*.gpg~ | ||
| /.gpg-v21-migrated | ||
| /private-keys-v1.d | ||
| /openpgp-revocs.d | ||
| /random_seed | ||
| /gnupg_spawn_agent_sentinel.lock | ||
| /tofu.db | ||
| /.#* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "schemaVersion": 2, | ||
| "mediaType": "application/vnd.docker.distribution.manifest.v2+json", | ||
| "config": { | ||
| "mediaType": "application/vnd.docker.container.image.v1+json", | ||
| "size": 7023, | ||
| "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" | ||
| }, | ||
| "layers": [ | ||
| { | ||
| "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | ||
| "size": 32654, | ||
| "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" | ||
| }, | ||
| { | ||
| "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | ||
| "size": 16724, | ||
| "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" | ||
| }, | ||
| { | ||
| "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", | ||
| "size": 73109, | ||
| "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,3 +204,59 @@ func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []b | |
| func (m *gpgmeSigningMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) { | ||
| return gpgUntrustedSignatureContents(untrustedSignature) | ||
| } | ||
|
|
||
| // isSubkeyOf checks if signerKeyIdentity is a subkey of expectedKeyIdentity | ||
| func (m *gpgmeSigningMechanism) isSubkeyOf(signerKeyIdentity, expectedKeyIdentity string) (bool, error) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading the GPGME implementation, yes, we’ll probably need to issue a |
||
| // Load the key for expectedKeyIdentity | ||
| key, err := m.ctx.GetKey(expectedKeyIdentity, false) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| // Check if signerKeyIdentity is a subkey of the primary key | ||
| // Get the first subkey (which should be the primary key itself) | ||
| subkey := key.SubKeys() | ||
|
|
||
| // Skip the primary key if needed and iterate through all subkeys | ||
| for subkey != nil { | ||
| // Check if the current subkey's fingerprint matches the signer's identity | ||
| if subkey.Fingerprint() == signerKeyIdentity { | ||
| return true, nil | ||
| } | ||
|
|
||
| // Move to the next subkey | ||
| subkey = subkey.Next() | ||
| } | ||
|
|
||
| // If we didn't find it as a subkey, check if it might be the primary key itself | ||
| // (in case the expectedKeyIdentity is actually a subkey) | ||
| key, err = m.ctx.GetKey(signerKeyIdentity, false) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| // Get the primary key fingerprint | ||
| primarySubkey := key.SubKeys() | ||
| if primarySubkey != nil && primarySubkey.Fingerprint() == expectedKeyIdentity { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t know whether we should support setting Also, this functionality directly contradicts the function name. Such a confusion in security-critical code is a risk in itself.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally you'd use
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me, the primary thing that matters is the (One could argue that the sanity-check is entirely unnecessary, I suppose.) The way the various implementations of Non-
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
? Google finds nothing.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies it's
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| return true, nil | ||
| } | ||
|
|
||
| return false, nil | ||
| } | ||
|
|
||
| // isKeyOrValidSubkey checks if the signerKeyIdentity is either the same as the expectedKeyIdentity, or is a valid | ||
| // subkey of the expectedKeyIdentity | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) |
||
| func isKeyOrValidSubkey(mech SigningMechanism, signerKeyIdentity, expectedKeyIdentity string) (bool, error) { | ||
| // If they're the same, no need to check subkey relationship | ||
| if signerKeyIdentity == expectedKeyIdentity { | ||
| return true, nil | ||
| } | ||
|
|
||
| // For gpgme mechanism, check subkey relationships | ||
| if gpgmeMech, ok := mech.(*gpgmeSigningMechanism); ok { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| return gpgmeMech.isSubkeyOf(signerKeyIdentity, expectedKeyIdentity) | ||
| } | ||
|
|
||
| // For other mechanisms, only accept exact matches | ||
| return false, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,7 +166,8 @@ func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents [ | |
| } | ||
|
|
||
| // Uppercase the fingerprint to be compatible with gpgme | ||
| return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil | ||
| keyIdentity = strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)) | ||
| return content, keyIdentity, nil | ||
| } | ||
|
|
||
| // UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION, | ||
|
|
@@ -177,3 +178,101 @@ func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents [ | |
| func (m *openpgpSigningMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) { | ||
| return gpgUntrustedSignatureContents(untrustedSignature) | ||
| } | ||
|
|
||
| // isSubkeyOf checks if signerKeyIdentity is a subkey of expectedKeyIdentity | ||
| func (m *openpgpSigningMechanism) isSubkeyOf(signerKeyIdentity, expectedKeyIdentity string) (bool, error) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that we can immediately read |
||
| // Convert fingerprints to lowercase for comparison | ||
| signerFingerprint := strings.ToLower(signerKeyIdentity) | ||
| expectedFingerprint := strings.ToLower(expectedKeyIdentity) | ||
|
|
||
| // If they're the same, it's a match | ||
| if signerFingerprint == expectedFingerprint { | ||
| return true, nil | ||
| } | ||
|
|
||
| // Find the entity with the expected fingerprint | ||
| var expectedEntity *openpgp.Entity | ||
| var signerEntity *openpgp.Entity | ||
|
|
||
| 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)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if primaryFingerprint == expectedFingerprint { | ||
| expectedEntity = entity | ||
| } | ||
| if primaryFingerprint == signerFingerprint { | ||
| signerEntity = entity | ||
| } | ||
| } | ||
|
|
||
| // Also check subkeys | ||
| for _, subkey := range entity.Subkeys { | ||
| if subkey.PublicKey != nil { | ||
| subkeyFingerprint := strings.ToLower(fmt.Sprintf("%x", subkey.PublicKey.Fingerprint)) | ||
| if subkeyFingerprint == expectedFingerprint { | ||
| expectedEntity = entity | ||
| } | ||
| if subkeyFingerprint == signerFingerprint { | ||
| signerEntity = entity | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // If we couldn't find either key, return false | ||
| if expectedEntity == nil || signerEntity == nil { | ||
| return false, nil | ||
| } | ||
|
|
||
| // Case 1: Both keys belong to the same entity - this is valid | ||
| if expectedEntity == signerEntity { | ||
| return true, nil | ||
| } | ||
|
|
||
| // Case 2: The signer is a subkey of the expected entity | ||
| if expectedEntity.PrimaryKey != nil { | ||
| expectedPrimaryFingerprint := strings.ToLower(fmt.Sprintf("%x", expectedEntity.PrimaryKey.Fingerprint)) | ||
| if expectedPrimaryFingerprint == expectedFingerprint { | ||
| // The expected key is a primary key, check if signer is one of its subkeys | ||
| for _, subkey := range expectedEntity.Subkeys { | ||
| if subkey.PublicKey != nil { | ||
| subkeyFingerprint := strings.ToLower(fmt.Sprintf("%x", subkey.PublicKey.Fingerprint)) | ||
| if subkeyFingerprint == signerFingerprint { | ||
| return true, nil | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Case 3: The expected key is a subkey, and the signer is the primary key of the same entity | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly to the GPG case, with |
||
| for _, subkey := range signerEntity.Subkeys { | ||
| if subkey.PublicKey != nil { | ||
| subkeyFingerprint := strings.ToLower(fmt.Sprintf("%x", subkey.PublicKey.Fingerprint)) | ||
| if subkeyFingerprint == expectedFingerprint { | ||
| // The expected key is a subkey of the signer's entity | ||
| return true, nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false, nil | ||
| } | ||
|
|
||
| // isKeyOrValidSubkey checks if the signerKeyIdentity is either the same as the expectedKeyIdentity, or is a valid | ||
| // subkey of the expectedKeyIdentity | ||
| func isKeyOrValidSubkey(mech SigningMechanism, signerKeyIdentity, expectedKeyIdentity string) (bool, error) { | ||
| // If they're the same, no need to check subkey relationship | ||
| if signerKeyIdentity == expectedKeyIdentity { | ||
| return true, nil | ||
| } | ||
|
|
||
| // For openpgp mechanism, check subkey relationships | ||
| if openpgpMech, ok := mech.(*openpgpSigningMechanism); ok { | ||
| return openpgpMech.isSubkeyOf(signerKeyIdentity, expectedKeyIdentity) | ||
| } | ||
|
|
||
| // For other mechanisms, only accept exact matches | ||
| return false, nil | ||
| } | ||
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.expectedKeyIdentity, so a subkey signature would still fail in this function.