bc-csharp icon indicating copy to clipboard operation
bc-csharp copied to clipboard

[BUG] PgpPublicKeyRing.GetPublicKeys() incorrectly sets isEncryptionKey on Primary Key

Open its-vincent opened this issue 5 months ago • 5 comments

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.

its-vincent avatar Jun 25 '25 12:06 its-vincent

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.

its-vincent avatar Jun 25 '25 13:06 its-vincent

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.

peterdettman avatar Jun 25 '25 16:06 peterdettman

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

its-vincent avatar Jul 06 '25 16:07 its-vincent

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.

peterdettman avatar Jul 07 '25 05:07 peterdettman

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?

its-vincent avatar Aug 27 '25 13:08 its-vincent