Skip to content

Commit

Permalink
Add SPHINCS+ signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
wussler committed Jan 2, 2023
1 parent 99d2669 commit 93315e7
Show file tree
Hide file tree
Showing 13 changed files with 640 additions and 40 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ go 1.13

require (
github.com/cloudflare/circl v1.2.0
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c h1:JCxCKz59IXghzSSUstoaWa7h7lZdmd0LFMiMfF56ECk=
github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c/go.mod h1:+SeUKO8dPlXRdYr4SK+UIs8SLz0Dl3ZceKdXGaSFsFY=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
15 changes: 14 additions & 1 deletion openpgp/key_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
goerrors "errors"
"github.com/ProtonMail/go-crypto/openpgp/dilithium_ecdsa"
"github.com/ProtonMail/go-crypto/openpgp/dilithium_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/sphincs_plus"
"io"
"math/big"
"time"
Expand Down Expand Up @@ -311,6 +312,18 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
}

return dilithium_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d)
case packet.PubKeyAlgoSphincsPlusSha2, packet.PubKeyAlgoSphincsPlusShake:
if !config.V5Keys {
return nil, goerrors.New("openpgp: cannot create a non-v5 sphincs+ key")
}

mode, err := packet.GetSphincsPlusModeFromAlgID(config.PublicKeyAlgorithm())
if err != nil {
return nil, err
}
parameter := config.SphincsPlusParam()

return sphincs_plus.GenerateKey(config.Random(), mode, parameter)
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
Expand Down Expand Up @@ -345,7 +358,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
return ecdh.GenerateKey(config.Random(), curve, kdf)
case packet.PubKeyAlgoDilithium3Ed25519, packet.PubKeyAlgoDilithium5Ed448, packet.PubKeyAlgoDilithium3p256,
packet.PubKeyAlgoDilithium5p384, packet.PubKeyAlgoDilithium3Brainpool256,
packet.PubKeyAlgoDilithium5Brainpool384:
packet.PubKeyAlgoDilithium5Brainpool384, packet.PubKeyAlgoSphincsPlusSha2, packet.PubKeyAlgoSphincsPlusShake:
if pubKeyAlgo, err = packet.GetMatchingKyberKem(config.PublicKeyAlgorithm()); err != nil {
return nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion openpgp/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ func (s *Subkey) IsPQ() bool {
case packet.PubKeyAlgoKyber768X25519, packet.PubKeyAlgoKyber1024X448, packet.PubKeyAlgoKyber768P256,
packet.PubKeyAlgoKyber1024P384, packet.PubKeyAlgoKyber768Brainpool256, packet.PubKeyAlgoKyber1024Brainpool384,
packet.PubKeyAlgoDilithium3Ed25519, packet.PubKeyAlgoDilithium5Ed448, packet.PubKeyAlgoDilithium3p256,
packet.PubKeyAlgoDilithium5p384, packet.PubKeyAlgoDilithium3Brainpool256, packet.PubKeyAlgoDilithium5Brainpool384:
packet.PubKeyAlgoDilithium5p384, packet.PubKeyAlgoDilithium3Brainpool256, packet.PubKeyAlgoDilithium5Brainpool384,
packet.PubKeyAlgoSphincsPlusSha2, packet.PubKeyAlgoSphincsPlusShake:
return true
default:
return false
Expand Down
70 changes: 40 additions & 30 deletions openpgp/keys_v5_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/dilithium_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/kyber_ecdh"
"github.com/ProtonMail/go-crypto/openpgp/sphincs_plus"
"io/ioutil"
"strings"
"testing"
Expand Down Expand Up @@ -207,7 +208,7 @@ func checkSerializeRead(t *testing.T, e *Entity) {
checkV5Key(t, el[0])
}

func TestGenerateDilithiumKey(t *testing.T) {
func TestGeneratePqKey(t *testing.T) {
randomPassword := make([]byte, 128)
_, err := rand.Read(randomPassword)
if err != nil {
Expand All @@ -221,11 +222,13 @@ func TestGenerateDilithiumKey(t *testing.T) {
"Dilithium5_P384":packet.PubKeyAlgoDilithium5p384,
"Dilithium3_Brainpool256": packet.PubKeyAlgoDilithium3Brainpool256,
"Dilithium5_Brainpool384":packet.PubKeyAlgoDilithium5Brainpool384,
"SphincsPlus_simple_SHA2":packet.PubKeyAlgoSphincsPlusSha2,
"SphincsPlus_simple_SHAKE":packet.PubKeyAlgoSphincsPlusShake,
}

for name, algo := range asymmAlgos {
t.Run(name, func(t *testing.T) {
dilithiumConfig := &packet.Config{
config := &packet.Config{
DefaultHash: crypto.SHA512,
Algorithm: algo,
V5Keys: true,
Expand All @@ -235,7 +238,7 @@ func TestGenerateDilithiumKey(t *testing.T) {
},
}

entity, err := NewEntity("Golang Gopher", "Test Key", "[email protected]", dilithiumConfig)
entity, err := NewEntity("Golang Gopher", "Test Key", "[email protected]", config)
if err != nil {
t.Fatal(err)
}
Expand All @@ -252,7 +255,7 @@ func TestGenerateDilithiumKey(t *testing.T) {
}

if read.PrimaryKey.PubKeyAlgo != algo {
t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, read.PrimaryKey.PubKeyAlgo)
t.Fatalf("Expected subkey algorithm: %v, got: %v", algo, read.PrimaryKey.PubKeyAlgo)
}

if err = read.PrivateKey.Encrypt(randomPassword); err != nil {
Expand Down Expand Up @@ -280,41 +283,48 @@ func TestGenerateDilithiumKey(t *testing.T) {
pk.PublicDilithium = pk.Dilithium.PublicKeyFromBytes(bin)
}

if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*sphincs_plus.PublicKey); ok {
pk.PublicData.PKseed[5] ^= 1
}

err = read.PrivateKey.Decrypt(randomPassword)
if _, ok := err.(errors.KeyInvalidError); !ok {
t.Fatal("Failed to detect invalid Dilithium key")
}

// Kyber subkey
subkey := read.Subkeys[0]
if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil {
t.Fatal(err)
}
testKyberSubkey(t, read.Subkeys[0], randomPassword)
})
}
}

if err := subkey.PrivateKey.Decrypt(randomPassword); err != nil {
t.Fatal("Valid Kyber key was marked as invalid: ", err)
}
func testKyberSubkey(t *testing.T, subkey Subkey, randomPassword []byte) {
var err error
if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil {
t.Fatal(err)
}

if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil {
t.Fatal(err)
}
if err = subkey.PrivateKey.Decrypt(randomPassword); err != nil {
t.Fatal("Valid Kyber key was marked as invalid: ", err)
}

// Corrupt public Kyber in primary key
if pk, ok := subkey.PublicKey.PublicKey.(*kyber_ecdh.PublicKey); ok {
bin, _ := pk.PublicKyber.MarshalBinary()
bin[5] ^= 1
if pk.PublicKyber, err = pk.Kyber.UnmarshalBinaryPublicKey(bin); err != nil {
t.Fatal("unable to corrupt key")
}
} else {
t.Fatal("Invalid subkey")
}
if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil {
t.Fatal(err)
}

err = subkey.PrivateKey.Decrypt(randomPassword)
if _, ok := err.(errors.KeyInvalidError); !ok {
t.Fatal("Failed to detect invalid Dilithium key")
}
})
// Corrupt public Kyber in primary key
if pk, ok := subkey.PublicKey.PublicKey.(*kyber_ecdh.PublicKey); ok {
bin, _ := pk.PublicKyber.MarshalBinary()
bin[5] ^= 1
if pk.PublicKyber, err = pk.Kyber.UnmarshalBinaryPublicKey(bin); err != nil {
t.Fatal("unable to corrupt key")
}
} else {
t.Fatal("Invalid subkey")
}

err = subkey.PrivateKey.Decrypt(randomPassword)
if _, ok := err.(errors.KeyInvalidError); !ok {
t.Fatal("Failed to detect invalid kyber key")
}
}

Expand Down
10 changes: 10 additions & 0 deletions openpgp/packet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package packet
import (
"crypto"
"crypto/rand"
"github.com/ProtonMail/go-crypto/openpgp/sphincs_plus"
"io"
"math/big"
"time"
Expand Down Expand Up @@ -55,6 +56,8 @@ type Config struct {
// Curve configures the desired packet.Curve if the Algorithm is PubKeyAlgoECDSA,
// PubKeyAlgoEdDSA, or PubKeyAlgoECDH. If empty Curve25519 is used.
Curve Curve
// SphincsPlusParameterId configures the desired sphincs plus security level parameter.
SphincsPlusParameterId sphincs_plus.ParameterSetId
// AEADConfig configures the use of the new AEAD Encrypted Data Packet,
// defined in the draft of the next version of the OpenPGP specification.
// If a non-nil AEADConfig is passed, usage of this packet is enabled. By
Expand Down Expand Up @@ -175,6 +178,13 @@ func (c *Config) CurveName() Curve {
return c.Curve
}

func (c *Config) SphincsPlusParam() sphincs_plus.ParameterSetId {
if c == nil || c.SphincsPlusParameterId == 0 {
return sphincs_plus.Parameter2
}
return c.SphincsPlusParameterId
}

func (c *Config) AEAD() *AEADConfig {
if c == nil {
return nil
Expand Down
5 changes: 4 additions & 1 deletion openpgp/packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ const (
PubKeyAlgoDilithium5p384 = 34
PubKeyAlgoDilithium3Brainpool256 = 35
PubKeyAlgoDilithium5Brainpool384 = 36
PubKeyAlgoSphincsPlusSha2 = 37
PubKeyAlgoSphincsPlusShake = 38
)

// CanEncrypt returns true if it's possible to encrypt a message to a public
Expand All @@ -452,7 +454,8 @@ func (pka PublicKeyAlgorithm) CanSign() bool {
switch pka {
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA,
PubKeyAlgoDilithium3Ed25519, PubKeyAlgoDilithium5Ed448, PubKeyAlgoDilithium3p256,
PubKeyAlgoDilithium5p384, PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384:
PubKeyAlgoDilithium5p384, PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384,
PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake:
return true
}
return false
Expand Down
52 changes: 52 additions & 0 deletions openpgp/packet/private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
goerrors "errors"
"github.com/ProtonMail/go-crypto/openpgp/dilithium_ecdsa"
"github.com/ProtonMail/go-crypto/openpgp/dilithium_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/sphincs_plus"
"io"
"io/ioutil"
"math/big"
Expand Down Expand Up @@ -130,6 +131,8 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
pk.PublicKey = *NewDilithiumECDSAPublicKey(creationTime, &pubkey.PublicKey)
case *dilithium_eddsa.PrivateKey:
pk.PublicKey = *NewDilithiumEdDSAPublicKey(creationTime, &pubkey.PublicKey)
case *sphincs_plus.PrivateKey:
pk.PublicKey = *NewSphincsPlusPublicKey(creationTime, &pubkey.PublicKey)
default:
panic("openpgp: unknown signer type in NewSignerPrivateKey")
}
Expand Down Expand Up @@ -377,6 +380,8 @@ func serializeECDHPrivateKey(w io.Writer, priv *ecdh.PrivateKey) error {
return err
}

// serializeKyberPrivateKey serializes a Kyber + ECC private key according to
// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#name-key-material-packets-7
func serializeKyberPrivateKey(w io.Writer, priv *kyber_ecdh.PrivateKey) (err error) {
var kyberBin []byte
if kyberBin, err = priv.SecretKyber.MarshalBinary(); err != nil {
Expand All @@ -389,6 +394,8 @@ func serializeKyberPrivateKey(w io.Writer, priv *kyber_ecdh.PrivateKey) (err err
return err
}

// serializeDilithiumECDSAPrivateKey serializes a Dilithium + ECDSA private key according to
// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-5.3.2
func serializeDilithiumECDSAPrivateKey(w io.Writer, priv *dilithium_ecdsa.PrivateKey) error {
if _, err := w.Write(encoding.NewOctetArray(priv.MarshalIntegerSecret()).EncodedBytes()); err != nil {
return err
Expand All @@ -397,6 +404,8 @@ func serializeDilithiumECDSAPrivateKey(w io.Writer, priv *dilithium_ecdsa.Privat
return err
}

// serializeDilithiumEdDSAPrivateKey serializes a Dilithium + EdDSA private key according to
// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-5.3.2
func serializeDilithiumEdDSAPrivateKey(w io.Writer, priv *dilithium_eddsa.PrivateKey) error {
if _, err := w.Write(encoding.NewOctetArray(priv.SecretEC).EncodedBytes()); err != nil {
return err
Expand All @@ -405,6 +414,19 @@ func serializeDilithiumEdDSAPrivateKey(w io.Writer, priv *dilithium_eddsa.Privat
return err
}

// serializeSphincsPlusPrivateKey serializes a SPHINCS+ private key according to
// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.2.2
func serializeSphincsPlusPrivateKey(w io.Writer, priv *sphincs_plus.PrivateKey) error {
privateData, err := priv.SerializePrivate()
if err != nil {
return err
}

_, err = w.Write(encoding.NewOctetArray(privateData).EncodedBytes())
return err
}


// Decrypt decrypts an encrypted private key using a passphrase.
func (pk *PrivateKey) Decrypt(passphrase []byte) error {
if pk.Dummy() {
Expand Down Expand Up @@ -544,6 +566,9 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) {
err = serializeDilithiumECDSAPrivateKey(w, priv)
case *dilithium_eddsa.PrivateKey:
err = serializeDilithiumEdDSAPrivateKey(w, priv)
case *sphincs_plus.PrivateKey:
err = serializeSphincsPlusPrivateKey(w, priv)

default:
err = errors.InvalidArgumentError("unknown private key type")
}
Expand Down Expand Up @@ -578,6 +603,8 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) {
return pk.parseDilithiumECDSAPrivateKey(data, 32, 4000)
case PubKeyAlgoDilithium5p384, PubKeyAlgoDilithium5Brainpool384:
return pk.parseDilithiumECDSAPrivateKey(data, 48, 4864)
case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake:
return pk.parseSphincsPlusPrivateKey(data)
}
panic("impossible")
}
Expand Down Expand Up @@ -824,6 +851,31 @@ func (pk *PrivateKey) parseKyberECDHPrivateKey(data []byte, ecLen, kLen int) (er
return nil
}

// parseSphincsPlusPrivateKey parses a Sphincs+ private key as specified in
// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.2.2
func (pk *PrivateKey) parseSphincsPlusPrivateKey(data []byte) (err error) {
if pk.Version != 5 {
return goerrors.New("openpgp: cannot parse non-v5 sphincs+ key")
}
pub := pk.PublicKey.PublicKey.(*sphincs_plus.PublicKey)
priv := new(sphincs_plus.PrivateKey)
priv.PublicKey = *pub

buf := bytes.NewBuffer(data)
spx := encoding.NewEmptyOctetArray(priv.ParameterSetId.GetSkLen())
if _, err := spx.ReadFrom(buf); err != nil {
return err
}

priv.UnmarshalPrivate(spx.Bytes())
if err := sphincs_plus.Validate(priv); err != nil {
return err
}
pk.PrivateKey = priv

return nil
}

func validateDSAParameters(priv *dsa.PrivateKey) error {
p := priv.P // group prime
q := priv.Q // subgroup order
Expand Down
Loading

0 comments on commit 93315e7

Please sign in to comment.