Skip to content

Commit e52eada

Browse files
authoredFeb 27, 2025··
Merge pull request #271 from ProtonMail/feat/improve-errors-key-selection
v2 API: Improve error messages for encryption key selection This PR enhances error messages by providing detailed explanations when key selection for encryption fails in the v2 API.
2 parents d47bb38 + 4bf9d90 commit e52eada

File tree

4 files changed

+108
-14
lines changed

4 files changed

+108
-14
lines changed
 

‎openpgp/errors/errors.go

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors"
77

88
import (
9+
"fmt"
910
"strconv"
1011
)
1112

@@ -178,3 +179,22 @@ type ErrMalformedMessage string
178179
func (dke ErrMalformedMessage) Error() string {
179180
return "openpgp: malformed message " + string(dke)
180181
}
182+
183+
// ErrEncryptionKeySelection is returned if encryption key selection fails (v2 API).
184+
type ErrEncryptionKeySelection struct {
185+
PrimaryKeyId string
186+
PrimaryKeyErr error
187+
EncSelectionKeyId *string
188+
EncSelectionErr error
189+
}
190+
191+
func (eks ErrEncryptionKeySelection) Error() string {
192+
prefix := fmt.Sprintf("openpgp: key selection for primary key %s:", eks.PrimaryKeyId)
193+
if eks.PrimaryKeyErr != nil {
194+
return fmt.Sprintf("%s invalid primary key: %s", prefix, eks.PrimaryKeyErr)
195+
}
196+
if eks.EncSelectionKeyId != nil {
197+
return fmt.Sprintf("%s invalid encryption key %s: %s", prefix, *eks.EncSelectionKeyId, eks.EncSelectionErr)
198+
}
199+
return fmt.Sprintf("%s no encryption key: %s", prefix, eks.EncSelectionErr)
200+
}

‎openpgp/v2/keys.go

+52-10
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,62 @@ func shouldPreferIdentity(existingId, potentialNewId *packet.Signature) bool {
9898
// EncryptionKey returns the best candidate Key for encrypting a message to the
9999
// given Entity.
100100
func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) {
101+
encryptionKey, err := e.EncryptionKeyWithError(now, config)
102+
return encryptionKey, err == nil
103+
}
104+
105+
// EncryptionKeyWithError returns the best candidate Key for encrypting a message to the
106+
// given Entity.
107+
// Provides an error if the function fails to find an encryption key.
108+
func (e *Entity) EncryptionKeyWithError(now time.Time, config *packet.Config) (Key, error) {
101109
// The primary key has to be valid at time now
102110
primarySelfSignature, err := e.VerifyPrimaryKey(now, config)
103111
if err != nil { // primary key is not valid
104-
return Key{}, false
112+
return Key{}, errors.ErrEncryptionKeySelection{
113+
PrimaryKeyId: e.PrimaryKey.KeyIdString(),
114+
PrimaryKeyErr: err,
115+
}
105116
}
106117

107-
if checkKeyRequirements(e.PrimaryKey, config) != nil {
118+
if err := checkKeyRequirements(e.PrimaryKey, config); err != nil {
108119
// The primary key produces weak signatures
109-
return Key{}, false
120+
return Key{}, errors.ErrEncryptionKeySelection{
121+
PrimaryKeyId: e.PrimaryKey.KeyIdString(),
122+
PrimaryKeyErr: err,
123+
}
110124
}
111125

112126
// Iterate the keys to find the newest, unexpired one
127+
var latestSelectionError *errors.ErrEncryptionKeySelection
113128
candidateSubkey := -1
114129
var maxTime time.Time
115130
var selectedSubkeySelfSig *packet.Signature
116131
for i, subkey := range e.Subkeys {
132+
subkeyErr := func(encSelectionErr error) *errors.ErrEncryptionKeySelection {
133+
subkeyKeyId := subkey.PublicKey.KeyIdString()
134+
return &errors.ErrEncryptionKeySelection{
135+
PrimaryKeyId: e.PrimaryKey.KeyIdString(),
136+
EncSelectionKeyId: &subkeyKeyId,
137+
EncSelectionErr: encSelectionErr,
138+
}
139+
140+
}
141+
// Verify the subkey signature.
117142
subkeySelfSig, err := subkey.Verify(now, config) // subkey has to be valid at time now
118-
if err == nil &&
119-
isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo, config) &&
120-
checkKeyRequirements(subkey.PublicKey, config) == nil &&
121-
(maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) {
143+
if err != nil {
144+
latestSelectionError = subkeyErr(err)
145+
continue
146+
}
147+
// Check the algorithm and key flags.
148+
if !isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo, config) {
149+
continue
150+
}
151+
// Check if the key fulfils the requirements
152+
if err := checkKeyRequirements(subkey.PublicKey, config); err != nil {
153+
latestSelectionError = subkeyErr(err)
154+
continue
155+
}
156+
if maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix() {
122157
candidateSubkey = i
123158
selectedSubkeySelfSig = subkeySelfSig
124159
maxTime = subkeySelfSig.CreationTime
@@ -133,7 +168,7 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool)
133168
PublicKey: subkey.PublicKey,
134169
PrivateKey: subkey.PrivateKey,
135170
SelfSignature: selectedSubkeySelfSig,
136-
}, true
171+
}, nil
137172
}
138173

139174
// If we don't have any subkeys for encryption and the primary key
@@ -145,10 +180,17 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool)
145180
PublicKey: e.PrimaryKey,
146181
PrivateKey: e.PrivateKey,
147182
SelfSignature: primarySelfSignature,
148-
}, true
183+
}, nil
184+
}
185+
186+
if latestSelectionError == nil {
187+
latestSelectionError = &errors.ErrEncryptionKeySelection{
188+
PrimaryKeyId: e.PrimaryKey.KeyIdString(),
189+
EncSelectionErr: goerrors.New("no encryption-capable key found (no key flags or invalid algorithm)"),
190+
}
149191
}
150192

151-
return Key{}, false
193+
return Key{}, latestSelectionError
152194
}
153195

154196
// DecryptionKeys returns all keys that are available for decryption, matching the keyID when given

‎openpgp/v2/keys_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -2075,3 +2075,37 @@ func TestAllowAllKeyFlagsWhenMissing(t *testing.T) {
20752075
t.Error("isValidCertificationKey must be true when InsecureAllowAllKeyFlagsWhenMissing is true")
20762076
}
20772077
}
2078+
2079+
func TestEncryptionKeyError(t *testing.T) {
2080+
// Make a master key.
2081+
entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil)
2082+
if err != nil {
2083+
t.Fatal(err)
2084+
}
2085+
2086+
_, err = entity.EncryptionKeyWithError(time.Unix(1405544146, 0), nil)
2087+
if err == nil {
2088+
t.Fatal("should fail")
2089+
}
2090+
if !strings.Contains(err.Error(), "no valid self signature found") {
2091+
t.Fatal("wrong error")
2092+
}
2093+
2094+
entity.Subkeys[0].PublicKey.Version = 20
2095+
_, err = entity.EncryptionKeyWithError(time.Now(), nil)
2096+
if err == nil {
2097+
t.Fatal("should fail")
2098+
}
2099+
if !strings.Contains(err.Error(), "no valid binding signature found for subkey") {
2100+
t.Fatal("wrong error")
2101+
}
2102+
2103+
entity.Subkeys = nil
2104+
_, err = entity.EncryptionKeyWithError(time.Now(), nil)
2105+
if err == nil {
2106+
t.Fatal("should fail")
2107+
}
2108+
if !strings.Contains(err.Error(), "no encryption-capable key found (no key flags or invalid algorithm)") {
2109+
t.Fatal("wrong error")
2110+
}
2111+
}

‎openpgp/v2/write.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -611,10 +611,8 @@ func encrypt(
611611
timeForEncryptionKey = *params.EncryptionTime
612612
}
613613
for i, recipient := range append(to, toHidden...) {
614-
var ok bool
615-
encryptKeys[i], ok = recipient.EncryptionKey(timeForEncryptionKey, config)
616-
if !ok {
617-
return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys")
614+
if encryptKeys[i], err = recipient.EncryptionKeyWithError(timeForEncryptionKey, config); err != nil {
615+
return nil, err
618616
}
619617

620618
primarySelfSignature, _ := recipient.PrimarySelfSignature(timeForEncryptionKey, config)

0 commit comments

Comments
 (0)
Please sign in to comment.