Skip to content

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

Open

Description

@its-vincent

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.

Activity

  1. its-vincent commented on Jun 25, 2025

    @its-vincent
    Author

    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
    ""
    gpg: encrypted with rsa4096 key, ID CCCCCCCCCCCCCCCC, created 2024-01-30
    ""
    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 "" [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
    ""
    gpg: encrypted with rsa4096 key, ID DDDDDDDDDDDDDDDD, created 2024-01-30
    ""
    gpg: Signature made 06/25/25 14:16:04 GMT Daylight Time
    gpg: using RSA key AAAAAAAAAAAAAAAA
    gpg: Good signature from "" [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.

  2. peterdettman commented on Jun 25, 2025

    @peterdettman
    Collaborator

    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.

  3. its-vincent commented on Jul 6, 2025

    @its-vincent
    Author

    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

  4. peterdettman commented on Jul 7, 2025

    @peterdettman
    Collaborator

    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.

  5. its-vincent commented on Aug 27, 2025

    @its-vincent
    Author

    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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions