[BUG] PgpPublicKeyRing.GetPublicKeys() incorrectly sets isEncryptionKey on Primary Key
Describe the Bug
When looping through the PgpPublicKeyRing.GetPublicKeys in a supplied Public Key File, the Primary Key is reported to possess a capability of the Subkey (Encryption)
GPG generates key pairs where the Primary is used for Authentication and Certification, and the subkey is used for Encryption.
According to https://github.com/gpg/gnupg/blob/master/doc/DETAILS which describes the output of a key listing --with-colums
Field 12 - Key capabilities The defined capabilities are:
e Encrypt s Sign c Certify a Authentication r Restricted encryption (subkey only use) t Timestamping g Group key ? Unknown capability A key may have any combination of them in any order. In addition to these letters, the primary key has uppercase versions of the letters to denote the usable capabilities of the entire key, and a potential letter ‘D’ to indicate a disabled key.
For a recipient of my encrypted file the output from :> gpg --list-keys --with-colons is
pub:f:4096:1:AAAAAAAAAAAAAAAA:1706608025:1864460825::-:::**scESC**::::::23::0:
fpr:::::::::<redacted>:
uid:f::::1706608027::<redacted>::<redacted>::::::::::0:
sub:f:4096:1:BBBBBBBBBBBBBBBB:1706608025:1864460825:::::**e**::::::23:
fpr:::::::::<redacted>:
The Primary key AAAAAAAAAAAAAAAA has Capabilities (s)ign and (c)ertify in the Capability Field highlighted with the ** characters (Bold doesn't work in Code). It also includes the overall key capabilities (ESC) which indicates that in addition to it's own (s) and (c), one of it's subkeys will have the (e)ncryption capability.
Subkey BBBBBBBBBBBBBBBB has Capability (e)
When looping through the PgpPublicKeyRing.GetPublicKeys using the following function I was originally checking for the first key that had the Capability IsEncryptionKey reported - BUT this was returning the Primary key. I had to work around the issue by adding additional code to check for a subkey with the encryption capability, and to return that.
` Private Function ReadPublicKeyFromFile(fileName As String) As PgpPublicKey Using keyIn = File.OpenRead(fileName) Console.WriteLine("") Console.WriteLine("Seeking Encryption Key ") Console.WriteLine("") Dim masterPublicKey As PgpPublicKey = Nothing Dim subPublicKey As PgpPublicKey = Nothing
Dim pubRings = New PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(keyIn))
For Each kRing As PgpPublicKeyRing In pubRings.GetKeyRings()
For Each key As PgpPublicKey In kRing.GetPublicKeys()
Console.WriteLine("Check Public KeyId: " & key.KeyId.ToString("X16"))
Console.WriteLine(" IsMasterKey: " & key.IsMasterKey.ToString)
Console.WriteLine(" IsEncrpytKey: " & key.IsEncryptionKey.ToString)
Console.WriteLine(" PubKeyAlgo: " & key.Algorithm.ToString)
Console.WriteLine(" PubKeyStrength: " & key.BitStrength.ToString)
Console.WriteLine(" PubKeyVersion: " & key.Version.ToString)
Console.WriteLine("")
'GPG default behaviour is to use a subkey for encryption/decryption - so make sure to check for a subkey with encryption capability
If key.IsEncryptionKey Then
If key.IsMasterKey Then
masterPublicKey = key
Else
subPublicKey = key
Console.WriteLine("Using SubKey KeyId: " & subPublicKey.KeyId.ToString("X16"))
Return key
End If
End If
Next
Next
'GPG default behaviour is to use a subkey for encryption/decryption
'If no subkey was found with the encryptionKey attribute set ten return the master key if that is capable of encrypting
If masterPublicKey IsNot Nothing Then
Console.WriteLine("Using Master KeyId: " & masterPublicKey.KeyId.ToString("X16"))
Return masterPublicKey
End If
End Using
Throw New ArgumentException("Can't find encryption key in key ring.")
End Function
`
This is the console output from the code
Seeking Encryption Key
Check Public KeyId: AAAAAAAAAAAAAAAA
IsMasterKey: True
**IsEncrpytKey: True**
PubKeyAlgo: RsaGeneral
PubKeyStrength: 4096
PubKeyVersion: 4
Check Public KeyId: BBBBBBBBBBBBBBBB
IsMasterKey: False
IsEncrpytKey: True
PubKeyAlgo: RsaGeneral
PubKeyStrength: 4096
PubKeyVersion: 4
Using SubKey KeyId: BBBBBBBBBBBBBBBB
As you can see from the output the Primary key (IsMasterKey: True) reports it has the encryption capability (IsEncryptKey: True) even though that capability belongs to the subkey. For the Primary key - IsEncryptKey should be False
The Subkey correctly reports that it is not the master key and it has the encryption capability
The result of this issue was that files got encrypted with the Primary key, and the recipient had to manually decrypt them as the referenced key does not have the (e)ncryption capability
To Reproduce
Steps to reproduce the behavior: Run the code above - or the equivalent in C#
Expected Behavior
The original function return
If key.IsEncryptionKey Then Return key End If
should not be seeing the Primary key as having the (e)ncryption capability and should not have returned until it was on the subkey.
Screenshots and Logs
If applicable, add screenshots and logs to help explain your problem.
Product Deployment
Please complete the following information:
- Deployment format: [Nuget package for Visual Studio 2022]
- Version [Latest stable 2.6.1]
Desktop
Please complete the following information:
- OS: [Windows 11 64-bit]
- Browser [N/A]
- Version [N/A]
Additional Context
Add any other context about the problem here.
When opening a file in Kleopatra that was encrypted with the Primary Key (that incorrectly reports having IsEncryptKey capability) - and clicking on the "Show Audit log" link it displays
gpg: encrypted with rsa4096 key, ID AAAAAAAAAAAAAAAA, created 2024-01-30 "<Redacted>" gpg: encrypted with rsa4096 key, ID CCCCCCCCCCCCCCCC, created 2024-01-30 "<Redacted>" gpg: used key is not marked for encryption use. gpg: Signature made 06/25/25 14:05:38 GMT Daylight Time gpg: using RSA key AAAAAAAAAAAAAAAA gpg: Good signature from "<Redacted>" [ultimate]
Note the gpg: used key is not marked for encryption use. warning reported.
When using the workaround code to skip the IsMasterKey ans look for a Subkey the results are as follows
gpg: encrypted with rsa4096 key, ID BBBBBBBBBBBBBBBB, created 2024-01-30 "<Redacted>" gpg: encrypted with rsa4096 key, ID DDDDDDDDDDDDDDDD, created 2024-01-30 "<Redacted>" gpg: Signature made 06/25/25 14:16:04 GMT Daylight Time gpg: using RSA key AAAAAAAAAAAAAAAA gpg: Good signature from "<Redacted>" [ultimate]
Note that the gpg: used key is not marked for encryption use. warning is not reported this time.
This workaround should not be required. The Primary Key object should not be getting assigned the IsEncryptionKey property in the first place.
I'm not an expert in PGP or even our OpenPgp API, but I am pretty sure you have made an incorrect assumption about what the IsEncryptionKey method does (or ought to do). The method doc (and implementation) show that it only regards the key's algorithm:
/// <summary>
/// Check if this key has an algorithm type that makes it suitable to use for encryption.
/// </summary>
/// <remarks>
/// Note: with version 4 keys KeyFlags subpackets should also be considered when present for
/// determining the preferred use of the key.
/// </remarks>
/// <returns>
/// <c>true</c> if this key algorithm is suitable for encryption.
/// </returns>
Note that the bc-csharp implementation is essentially downstream of the bc-java one (being a port) and notably behind on some features. However a quick comparison shows that the equivalent bc-java method also only looks at the algorithm.
You quote
/// <returns> /// <c>true</c> if this key algorithm is suitable for encryption. /// </returns>
This is exactly my point. According to the GnuPG spec - this key is not suitable for encryption so this function should have returned FALSE
It says the key algorithm is suitable for encryption. The method could probably be Obsoleted/renamed and another method added that means something stronger; PRs welcome.
Based on your feedback and this remark
/// <remarks> /// Note: with version 4 keys KeyFlags subpackets should also be considered when present for /// determining the preferred use of the key. /// </remarks>
I spent some more time hacking with the API - trying to figure out what it meant/how to interpret it.
This is what I came up with.
Private Function ReadPublicKeyFromFile(fileName As String) As PgpPublicKey
Using keyIn = File.OpenRead(fileName)
Console.WriteLine("")
Console.WriteLine("Seeking Encryption Key ")
Console.WriteLine("")
Dim masterPublicKey As PgpPublicKey = Nothing
Dim subPublicKey As PgpPublicKey = Nothing
Dim subPacketVector As PgpSignatureSubpacketVector
Dim keyFlags As EnumKeyFlags
Dim pubRings = New PgpPublicKeyRingBundle(PgpUtilities.GetDecoderStream(keyIn))
Dim canEncryptComms As Boolean
For Each kRing As PgpPublicKeyRing In pubRings.GetKeyRings()
For Each key As PgpPublicKey In kRing.GetPublicKeys()
canEncryptComms = False
Console.WriteLine("Check Public KeyId: " & key.KeyId.ToString("X16"))
Console.WriteLine(" IsMasterKey: " & key.IsMasterKey.ToString)
Console.WriteLine(" IsEncrpytKey: " & key.IsEncryptionKey.ToString)
Console.WriteLine(" PubKeyAlgo: " & key.Algorithm.ToString)
Console.WriteLine(" PubKeyStrength: " & key.BitStrength.ToString)
Console.WriteLine(" PubKeyVersion: " & key.Version.ToString)
If key.Version >= 4 Then
For Each myPgpSignature As PgpSignature In key.GetSignatures()
If myPgpSignature.HasSubpackets Then
subPacketVector = myPgpSignature.GetHashedSubPackets()
keyFlags = subPacketVector.GetKeyFlags()
Console.WriteLine(" MyKeyFlags: " & keyFlags.ToString())
If (keyFlags And EnumKeyFlags.certifyKeys) Then
Console.WriteLine(" MyKeyFlags: certifyKey")
End If
If (keyFlags And EnumKeyFlags.signData) Then
Console.WriteLine(" MyKeyFlags: signData")
End If
If (keyFlags And EnumKeyFlags.encryptComms) Then
Console.WriteLine(" MyKeyFlags: encryptComms")
canEncryptComms = True
End If
If (keyFlags And EnumKeyFlags.encryptStorage) Then
Console.WriteLine(" MyKeyFlags: encryptStorage")
End If
If canEncryptComms Then
Console.WriteLine("")
Console.WriteLine("Found a key/subkey with the encryptComms KeyFlag set... Exiting loop!")
Exit For
End If
End If
Next
End If
Console.WriteLine("")
'GPG default behaviour is to use a subkey for encryption/decryption - so make sure to check for a subkey with encryption capability
If key.IsEncryptionKey And canEncryptComms Then
If key.IsMasterKey Then
masterPublicKey = key
'Console.WriteLine("Using Master KeyId: " & masterPublicKey.KeyId.ToString("X16"))
'Return key
Else
subPublicKey = key
Console.WriteLine("Using SubKey KeyId: " & subPublicKey.KeyId.ToString("X16"))
Return key
End If
End If
Next
Next
'GPG default behaviour is to use a subkey for encryption/decryption
'If no subkey was found with the encryptionKey attribute set ten return the master key if that is capable of encrypting
If masterPublicKey IsNot Nothing Then
Console.WriteLine("Using Master KeyId: " & masterPublicKey.KeyId.ToString("X16"))
Return masterPublicKey
End If
End Using
Throw New ArgumentException("Can't find encryption key in key ring.")
End Function
This updated code yields the following output
Seeking Encryption Key
Check Public KeyId: F930B36C1ACBA528
IsMasterKey: True
IsEncrpytKey: True
PubKeyAlgo: RsaGeneral
PubKeyStrength: 4096
PubKeyVersion: 4
MyKeyFlags: 3
MyKeyFlags: certifyKey
MyKeyFlags: signData
Check Public KeyId: 1CC9A836270CDD4D
IsMasterKey: False
IsEncrpytKey: True
PubKeyAlgo: RsaGeneral
PubKeyStrength: 4096
PubKeyVersion: 4
MyKeyFlags: 12
MyKeyFlags: encryptComms
MyKeyFlags: encryptStorage
Found a key/subkey with the encryptComms KeyFlag set... Exiting loop!
Using SubKey KeyId: 1CC9A836270CDD4D
Is this the correct approach?
Are there any other safeguards that should be added to the code? How can I test if KeyFlags are set for the key before trying to enumerate the content? What approach should be taken if the key is not a Version 4 key?