Skip to content
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

Randomize v4 and v5 signatures via custom notation #209

Merged
merged 4 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion openpgp/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,9 @@ func TestNewEntityPrivateSerialization(t *testing.T) {
}

func TestNotationPacket(t *testing.T) {
config := &packet.Config{
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithNotation))
if err != nil {
t.Fatal(err)
Expand All @@ -974,7 +977,7 @@ func TestNotationPacket(t *testing.T) {
assertNotationPackets(t, keys)

serializedEntity := bytes.NewBuffer(nil)
err = keys[0].SerializePrivate(serializedEntity, nil)
err = keys[0].SerializePrivate(serializedEntity, config)
if err != nil {
t.Fatal(err)
}
Expand Down
21 changes: 21 additions & 0 deletions openpgp/packet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ type Config struct {
// that the packet sequence conforms with the grammar mandated by rfc4880.
// The default behavior, when the config or flag is nil, is to check the packet sequence.
CheckPacketSequence *bool
// NonDeterministicSignaturesViaNotation is a flag to enable randomization of signatures.
// If true, a salt notation is used to randomize signatures generated by v4 and v5 keys
// (v6 signatures are always non-deterministic, by design).
// This protects EdDSA signatures from potentially leaking the secret key in case of faults (i.e. bitflips) which, in principle, could occur
// during the signing computation. It is added to signatures of any algo for simplicity, and as it may also serve as protection in case of
// weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
// The default behavior, when the config or flag is nil, is to enable the feature.
NonDeterministicSignaturesViaNotation *bool
}

func (c *Config) Random() io.Reader {
Expand Down Expand Up @@ -359,3 +367,16 @@ func (c *Config) StrictPacketSequence() bool {
}
return *c.CheckPacketSequence
}

func (c *Config) RandomizeSignaturesViaNotation() bool {
if c == nil || c.NonDeterministicSignaturesViaNotation == nil {
return true
}
return *c.NonDeterministicSignaturesViaNotation
}

// BoolPointer is a helper function to set a boolean pointer in the Config.
// e.g., config.CheckPacketSequence = BoolPointer(true)
func BoolPointer(value bool) *bool {
return &value
}
40 changes: 40 additions & 0 deletions openpgp/packet/private_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,43 @@ func TestElGamalValidation(t *testing.T) {
}
priv.Y = &y
}

func TestECDSASignerRandomizedNotation(t *testing.T) {
ecdsaPriv, err := ecdsa.GenerateKey(rand.Reader, ecc.NewGenericCurve(elliptic.P256()))
if err != nil {
t.Fatal(err)
}

priv := NewSignerPrivateKey(time.Now(), ecdsaPriv)
sig := &Signature{
Version: 4,
PubKeyAlgo: PubKeyAlgoECDSA,
Hash: crypto.SHA256,
}
msg := make([]byte, mathrand.Intn(maxMessageLength))
rand.Read(msg)

h, err := populateHash(sig.Hash, msg)
if err != nil {
t.Fatal(err)
}
config := Config{
NonDeterministicSignaturesViaNotation: BoolPointer(true),
}
if err := sig.Sign(h, priv, &config); err != nil {
t.Fatal(err)
}

if h, err = populateHash(sig.Hash, msg); err != nil {
t.Fatal(err)
}
if err := priv.VerifySignature(h, sig); err != nil {
t.Fatal(err)
}
if len(sig.Notations) == 0 {
t.Fatalf("failed to find randomized notation")
}
if sig.Notations[0].Name != SaltNotationName {
t.Fatalf("failed to find randomized notation")
}
}
30 changes: 30 additions & 0 deletions openpgp/packet/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const (
KeyFlagGroupKey
)

const SaltNotationName = "[email protected]"

// Signature represents a signature. See RFC 4880, section 5.2.
type Signature struct {
Version int
Expand Down Expand Up @@ -882,6 +884,20 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e
}
sig.Version = priv.PublicKey.Version
sig.IssuerFingerprint = priv.PublicKey.Fingerprint
if sig.Version < 6 && config.RandomizeSignaturesViaNotation() {
sig.removeNotationsWithName(SaltNotationName)
salt, err := SignatureSaltForHash(sig.Hash, config.Random())
if err != nil {
return err
}
notation := Notation{
Name: SaltNotationName,
Value: salt,
IsCritical: false,
IsHumanReadable: false,
}
sig.Notations = append(sig.Notations, &notation)
}
sig.outSubpackets, err = sig.buildSubpackets(priv.PublicKey)
if err != nil {
return err
Expand Down Expand Up @@ -1409,3 +1425,17 @@ func SignatureSaltForHash(hash crypto.Hash, randReader io.Reader) ([]byte, error
}
return salt, nil
}

// removeNotationsWithName removes all notations in this signature with the given name.
func (sig *Signature) removeNotationsWithName(name string) {
if sig == nil || sig.Notations == nil {
return
}
updatedNotations := make([]*Notation, 0, len(sig.Notations))
for _, notation := range sig.Notations {
if notation.Name != name {
updatedNotations = append(updatedNotations, notation)
}
}
sig.Notations = updatedNotations
}
5 changes: 4 additions & 1 deletion openpgp/v2/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,9 @@ func TestNewEntityPrivateSerialization(t *testing.T) {
}

func TestNotationPacket(t *testing.T) {
keySerializeConfig := &packet.Config{
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithNotation))
if err != nil {
t.Fatal(err)
Expand All @@ -938,7 +941,7 @@ func TestNotationPacket(t *testing.T) {
assertNotationPackets(t, keys)

serializedEntity := bytes.NewBuffer(nil)
err = keys[0].SerializePrivate(serializedEntity, nil)
err = keys[0].SerializePrivate(serializedEntity, keySerializeConfig)
if err != nil {
t.Fatal(err)
}
Expand Down
11 changes: 8 additions & 3 deletions openpgp/v2/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func TestSignDetachedWithNotation(t *testing.T) {
IsHumanReadable: true,
},
}
config.NonDeterministicSignaturesViaNotation = packet.BoolPointer(false)
err := DetachSign(signature, kring[:1], message, &config)
if err != nil {
t.Error(err)
Expand Down Expand Up @@ -138,6 +139,7 @@ func TestSignDetachedWithCriticalNotation(t *testing.T) {
IsCritical: true,
},
}
config.NonDeterministicSignaturesViaNotation = packet.BoolPointer(false)
err := DetachSign(signature, kring[:1], message, &config)
if err != nil {
t.Error(err)
Expand Down Expand Up @@ -209,8 +211,11 @@ func TestNewEntity(t *testing.T) {
t.Errorf("BitLength %v, expected %v", bl, cfg.RSABits)
}

keySerializeConfig := &packet.Config{
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
w := bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize entity: %s", err)
return
}
Expand All @@ -227,7 +232,7 @@ func TestNewEntity(t *testing.T) {
}

w = bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize entity second time: %s", err)
return
}
Expand All @@ -245,7 +250,7 @@ func TestNewEntity(t *testing.T) {
}

w = bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize after encryption round: %s", err)
return
}
Expand Down
12 changes: 8 additions & 4 deletions openpgp/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func TestSignDetachedWithNotation(t *testing.T) {
IsHumanReadable: true,
},
},
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
err := DetachSign(signature, kring[0], message, config)
if err != nil {
Expand Down Expand Up @@ -137,6 +138,7 @@ func TestSignDetachedWithCriticalNotation(t *testing.T) {
IsCritical: true,
},
},
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
err := DetachSign(signature, kring[0], message, config)
if err != nil {
Expand Down Expand Up @@ -180,7 +182,6 @@ func TestSignDetachedWithCriticalNotation(t *testing.T) {
}

func TestNewEntity(t *testing.T) {

// Check bit-length with no config.
e, err := NewEntity("Test User", "test", "[email protected]", nil)
if err != nil {
Expand Down Expand Up @@ -211,8 +212,11 @@ func TestNewEntity(t *testing.T) {
t.Errorf("BitLength %v, expected %v", bl, cfg.RSABits)
}

keySerializeConfig := &packet.Config{
NonDeterministicSignaturesViaNotation: packet.BoolPointer(false),
}
w := bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize entity: %s", err)
return
}
Expand All @@ -229,7 +233,7 @@ func TestNewEntity(t *testing.T) {
}

w = bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize entity second time: %s", err)
return
}
Expand All @@ -247,7 +251,7 @@ func TestNewEntity(t *testing.T) {
}

w = bytes.NewBuffer(nil)
if err := e.SerializePrivate(w, nil); err != nil {
if err := e.SerializePrivate(w, keySerializeConfig); err != nil {
t.Errorf("failed to serialize after encryption round: %s", err)
return
}
Expand Down
Loading