diff --git a/go.mod b/go.mod index 18a32833..66636cf8 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index fb596562..110c3a60 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 78265635..f06fff0d 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -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" @@ -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") } @@ -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 } diff --git a/openpgp/keys.go b/openpgp/keys.go index dce7db9a..cb4b99a3 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -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 diff --git a/openpgp/keys_v5_test.go b/openpgp/keys_v5_test.go index b6868654..3825d869 100644 --- a/openpgp/keys_v5_test.go +++ b/openpgp/keys_v5_test.go @@ -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" @@ -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 { @@ -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, @@ -235,7 +238,7 @@ func TestGenerateDilithiumKey(t *testing.T) { }, } - entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", dilithiumConfig) + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) if err != nil { t.Fatal(err) } @@ -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 { @@ -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") } } diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index f9208158..bc9ec3b5 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -7,6 +7,7 @@ package packet import ( "crypto" "crypto/rand" + "github.com/ProtonMail/go-crypto/openpgp/sphincs_plus" "io" "math/big" "time" @@ -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 @@ -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 diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 6071de69..77ed7640 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -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 @@ -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 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 7d26b77e..8862ff1c 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -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" @@ -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") } @@ -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 { @@ -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 @@ -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 @@ -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() { @@ -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") } @@ -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") } @@ -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 diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 6b71c167..734b3d1d 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -18,6 +18,7 @@ import ( "github.com/ProtonMail/go-crypto/brainpool" "github.com/ProtonMail/go-crypto/openpgp/dilithium_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/dilithium_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/sphincs_plus" "github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem/kyber/kyber1024" "github.com/cloudflare/circl/kem/kyber/kyber768" @@ -62,6 +63,9 @@ type PublicKey struct { // kdf stores key derivation function parameters // used for ECDH encryption. See RFC 6637, Section 9. kdf encoding.Field + + // sphincsPlusParameterSetId contains the parameter set ID for the sphincs+ instantiation + sphincsPlusParameterSetId sphincs_plus.ParameterSetId } // UpgradeToV5 updates the version of the key to v5, and updates all necessary @@ -237,6 +241,27 @@ func NewDilithiumEdDSAPublicKey(creationTime time.Time, pub *dilithium_eddsa.Pub return pk } +func NewSphincsPlusPublicKey(creationTime time.Time, pub *sphincs_plus.PublicKey) *PublicKey { + var pk *PublicKey + + publicData, err := pub.SerializePublic() + if err != nil { + panic("generated invalid sphincs+ public key") + } + + pk = &PublicKey{ + Version: 5, + CreationTime: creationTime, + PubKeyAlgo: GetAlgIDFromSphincsPlusMode(pub.Mode), + PublicKey: pub, + p: encoding.NewOctetArray(publicData), + sphincsPlusParameterSetId: pub.ParameterSetId, + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -287,6 +312,10 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseDilithiumECDSA(r, 65, 1952) case PubKeyAlgoDilithium5p384, PubKeyAlgoDilithium5Brainpool384: err = pk.parseDilithiumECDSA(r, 97, 2592) + case PubKeyAlgoSphincsPlusSha2: + err = pk.parseSphincsPlus(r, sphincs_plus.ModeSimpleSHA2) + case PubKeyAlgoSphincsPlusShake: + err = pk.parseSphincsPlus(r, sphincs_plus.ModeSimpleShake) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -545,7 +574,7 @@ func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) { return } -// parseKyberECDH parses a Dilithium + ECDSA public key as specified in +// parseDilithiumECDSA parses a Dilithium + ECDSA public key as specified in // https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-5.3.2 func (pk *PublicKey) parseDilithiumECDSA(r io.Reader, ecLen, dLen int) (err error) { pk.p = encoding.NewEmptyOctetArray(ecLen) @@ -581,7 +610,7 @@ func (pk *PublicKey) parseDilithiumECDSA(r io.Reader, ecLen, dLen int) (err erro return } -// parseKyberECDH parses a Dilithium + EdDSA public key as specified in +// parseDilithiumEdDSA parses a Dilithium + EdDSA public key as specified in // https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-5.3.2 func (pk *PublicKey) parseDilithiumEdDSA(r io.Reader, ecLen, dLen int) (err error) { pk.p = encoding.NewEmptyOctetArray(ecLen) @@ -614,6 +643,39 @@ func (pk *PublicKey) parseDilithiumEdDSA(r io.Reader, ecLen, dLen int) (err erro return } +// parseSphincsPlus parses a SPHINCS+ public key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.2.2 +func (pk *PublicKey) parseSphincsPlus(r io.Reader, mode sphincs_plus.Mode) (err error) { + var id sphincs_plus.ParameterSetId + pub := new(sphincs_plus.PublicKey) + + var param [1]byte + if _, err = readFull(r, param[:]); err != nil { + return + } + + if id, err = sphincs_plus.ParseParameterSetID(param); err != nil { + return + } + + pk.sphincsPlusParameterSetId = id + pub.ParameterSetId = id + pub.Mode = mode + pub.Parameters, err = sphincs_plus.GetParametersFromModeAndId(mode, id) + + pk.p = encoding.NewEmptyOctetArray(pub.ParameterSetId.GetPkLen()) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + if err := pub.UnmarshalPublic(pk.p.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub + return +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -689,6 +751,9 @@ func (pk *PublicKey) algorithmSpecificByteCount() int { PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384: length += int(pk.p.EncodedLength()) length += int(pk.q.EncodedLength()) + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + length += 1 // ParamID octet + length += int(pk.p.EncodedLength()) default: panic("unknown public key algorithm") } @@ -774,6 +839,12 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(pk.q.EncodedBytes()) return + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + if _, err = w.Write(pk.sphincsPlusParameterSetId.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.p.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -846,6 +917,13 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("dilithium_ecdsa verification failure") } return nil + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + spxPublicKey := pk.PublicKey.(*sphincs_plus.PublicKey) + if sig.sphincsPlusParameterSetId != spxPublicKey.ParameterSetId || + !sphincs_plus.Verify(spxPublicKey, hashBytes, sig.SphincsPlusSig.Bytes()) { + return errors.SignatureError("sphincs+ verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -998,6 +1076,8 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { PubKeyAlgoDilithium5Ed448, PubKeyAlgoDilithium3p256, PubKeyAlgoDilithium5p384, PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384: bitLength = pk.q.BitLength() // Very questionable + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + bitLength = pk.p.BitLength() // Even more questionable default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1021,7 +1101,7 @@ func GetMatchingKyberKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { switch algId { case PubKeyAlgoDilithium3Ed25519: return PubKeyAlgoKyber768X25519, nil - case PubKeyAlgoDilithium5Ed448: + case PubKeyAlgoDilithium5Ed448, PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: return PubKeyAlgoKyber1024X448, nil case PubKeyAlgoDilithium3p256: return PubKeyAlgoKyber768P256, nil @@ -1032,7 +1112,7 @@ func GetMatchingKyberKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { case PubKeyAlgoDilithium5Brainpool384: return PubKeyAlgoKyber1024Brainpool384, nil default: - return 0, goerrors.New("packet: unsupported Kyber public key algorithm") + return 0, goerrors.New("packet: unsupported pq public key algorithm") } } @@ -1094,6 +1174,28 @@ func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { } } +func GetSphincsPlusModeFromAlgID(algId PublicKeyAlgorithm) (sphincs_plus.Mode, error) { + switch algId { + case PubKeyAlgoSphincsPlusSha2: + return sphincs_plus.ModeSimpleSHA2, nil + case PubKeyAlgoSphincsPlusShake: + return sphincs_plus.ModeSimpleShake, nil + default: + return 0, goerrors.New("packet: unsupported EdDSA public key algorithm") + } +} + +func GetAlgIDFromSphincsPlusMode(mode sphincs_plus.Mode) PublicKeyAlgorithm { + switch mode { + case sphincs_plus.ModeSimpleSHA2: + return PubKeyAlgoSphincsPlusSha2 + case sphincs_plus.ModeSimpleShake: + return PubKeyAlgoSphincsPlusShake + default: + panic("invalid sphincs+ mode") + } +} + func GetDilithiumFromAlgID(algId PublicKeyAlgorithm) (dilithium.Mode, error) { switch algId { diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 2ee2effd..34cb3aac 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -11,6 +11,7 @@ import ( "encoding/binary" "github.com/ProtonMail/go-crypto/openpgp/dilithium_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/dilithium_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/sphincs_plus" "hash" "io" "strconv" @@ -59,7 +60,11 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field - DilithumSig encoding.Field + DilithumSig encoding.Field + SphincsPlusSig encoding.Field + + // sphincsPlusParameterSetId contains the parameter set ID for the sphincs+ instantiation + sphincsPlusParameterSetId sphincs_plus.ParameterSetId // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -124,7 +129,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoDilithium3Ed25519, PubKeyAlgoDilithium5Ed448, PubKeyAlgoDilithium3p256, PubKeyAlgoDilithium5p384, - PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384: + PubKeyAlgoDilithium3Brainpool256, PubKeyAlgoDilithium5Brainpool384, PubKeyAlgoSphincsPlusSha2, + PubKeyAlgoSphincsPlusShake: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -214,6 +220,10 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err = sig.parseDilithiumEcdsaSignature(r, 48, 4595); err != nil { return } + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + if err = sig.parseSphincsPlusSignature(r); err != nil { + return + } default: panic("unreachable") } @@ -251,6 +261,23 @@ func (sig *Signature) parseDilithiumEcdsaSignature(r io.Reader, ecLen, dLen int) return } +// parseSphincsPlusSignature parses a SPHINCS+ signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.2.1 +func (sig *Signature) parseSphincsPlusSignature(r io.Reader) (err error) { + var param [1]byte + if _, err = readFull(r, param[:]); err != nil { + return + } + + if sig.sphincsPlusParameterSetId, err = sphincs_plus.ParseParameterSetID(param); err != nil { + return + } + + sig.SphincsPlusSig = encoding.NewEmptyOctetArray(sig.sphincsPlusParameterSetId.GetSigLen()) + _, err = sig.SphincsPlusSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 4880, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -737,6 +764,14 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sig.DilithumSig = encoding.NewOctetArray(dSig) sig.EdDSASigR = encoding.NewOctetArray(ecSig) } + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + sk := priv.PrivateKey.(*sphincs_plus.PrivateKey) + spxSig, err := sphincs_plus.Sign(sk, digest) + + if err == nil { + sig.sphincsPlusParameterSetId = sk.ParameterSetId + sig.SphincsPlusSig = encoding.NewOctetArray(spxSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -810,7 +845,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.SphincsPlusSig == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -835,6 +870,9 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = int(sig.ECDSASigR.EncodedLength()) sigLength += int(sig.ECDSASigS.EncodedLength()) sigLength += int(sig.DilithumSig.EncodedLength()) + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + sigLength = 1 // Parameter ID + sigLength += int(sig.SphincsPlusSig.EncodedLength()) default: panic("impossible") } @@ -912,6 +950,11 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.DilithumSig.EncodedBytes()) + case PubKeyAlgoSphincsPlusSha2, PubKeyAlgoSphincsPlusShake: + if _, err = w.Write(sig.sphincsPlusParameterSetId.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.SphincsPlusSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/sphincs_plus/parameter.go b/openpgp/sphincs_plus/parameter.go new file mode 100644 index 00000000..62ddca46 --- /dev/null +++ b/openpgp/sphincs_plus/parameter.go @@ -0,0 +1,85 @@ +// Package sphincs_plus implements SPHINCS+ suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#name-sphincs-8 +package sphincs_plus + +import ( + goerrors "errors" +) + +// ParameterSetId represents the security level parameters defined in: +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#table-13 +type ParameterSetId uint8 +const ( + Parameter1 ParameterSetId = 1 + Parameter2 ParameterSetId = 2 + Parameter3 ParameterSetId = 3 + Parameter4 ParameterSetId = 4 + Parameter5 ParameterSetId = 5 + Parameter6 ParameterSetId = 6 +) + +// ParseParameterSetID parses the ParameterSetId from a byte, returning an error if it's not recognised +func ParseParameterSetID(data [1]byte) (setId ParameterSetId, err error) { + setId = ParameterSetId(data[0]) + switch setId { + case Parameter1, Parameter2, Parameter3, Parameter4, Parameter5, Parameter6: + return setId, nil + default: + return 0, goerrors.New("packet: unsupported sphincs+ parameter id") + } +} + +// GetPkLen returns the size of the public key in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.1 +func (setId ParameterSetId) GetPkLen() int { + switch setId { + case Parameter1, Parameter2: + return 32 + case Parameter3, Parameter4: + return 48 + case Parameter5, Parameter6: + return 64 + default: + panic("sphincs_plus: unsupported parameter") + } +} + +// GetSkLen returns the size of the secret key in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.1 +func (setId ParameterSetId) GetSkLen() int { + switch setId { + case Parameter1, Parameter2: + return 64 + case Parameter3, Parameter4: + return 96 + case Parameter5, Parameter6: + return 128 + default: + panic("sphincs_plus: unsupported parameter") + } +} + +// GetSigLen returns the size of the signature in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.1 +func (setId ParameterSetId) GetSigLen() int { + switch setId { + case Parameter1: + return 7856 + case Parameter2: + return 17088 + case Parameter3: + return 16224 + case Parameter4: + return 35664 + case Parameter5: + return 29792 + case Parameter6: + return 49856 + default: + panic("sphincs_plus: unsupported parameter") + } +} + +func (setId ParameterSetId) EncodedBytes() []byte { + return []byte{byte(setId)} +} diff --git a/openpgp/sphincs_plus/sphincs.go b/openpgp/sphincs_plus/sphincs.go new file mode 100644 index 00000000..cda96479 --- /dev/null +++ b/openpgp/sphincs_plus/sphincs.go @@ -0,0 +1,153 @@ +// Package sphincs_plus implements SPHINCS+ suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#name-sphincs-8 +package sphincs_plus + +import ( + "crypto/subtle" + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/kasperdi/SPHINCSPLUS-golang/parameters" + "github.com/kasperdi/SPHINCSPLUS-golang/sphincs" +) + +// Mode defines the underlying hash and mode depending on the algorithm ID as specified here: +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-2.2 +type Mode uint8 +const ( + ModeSimpleSHA2 Mode = 1 + ModeSimpleShake Mode = 2 +) + +type PublicKey struct { + ParameterSetId ParameterSetId + Mode Mode + Parameters *parameters.Parameters + PublicData *sphincs.SPHINCS_PK +} + +type PrivateKey struct { + PublicKey + SecretData *sphincs.SPHINCS_SK +} + +func (priv *PrivateKey) SerializePrivate ()([]byte, error) { + return priv.SecretData.SerializeSK() +} + +func (priv *PrivateKey) UnmarshalPrivate (data []byte) (err error) { + // Copy data to prevent library from using an older reference + serialized := make([]byte, len(data)) + copy(serialized, data) + + priv.SecretData, err = sphincs.DeserializeSK(priv.Parameters, serialized) + if err != nil { + return err + } + + return nil +} + +func (pub *PublicKey) SerializePublic ()([]byte, error) { + return pub.PublicData.SerializePK() +} + +func (pub *PublicKey) UnmarshalPublic (data []byte) (err error) { + // Copy data to prevent library from using an older reference + serialized := make([]byte, len(data)) + copy(serialized, data) + + pub.PublicData, err = sphincs.DeserializePK(pub.Parameters, serialized) + if err != nil { + return err + } + + return nil +} + +func GenerateKey(_ io.Reader, mode Mode, param ParameterSetId) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.ParameterSetId = param + priv.Mode = mode + if priv.Parameters, err = GetParametersFromModeAndId(mode, param); err != nil { + return nil, err + } + + // TODO: add error handling to library + // TODO: accept external randomness source + priv.SecretData, priv.PublicData = sphincs.Spx_keygen(priv.Parameters) + + return +} + +// Sign generates a SPHINCS+ signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.1.2 +func Sign(priv *PrivateKey, message []byte) ([]byte, error) { + sig := sphincs.Spx_sign(priv.Parameters, message, priv.SecretData) + return sig.SerializeSignature() +} + +// Verify verifies a SPHINCS+ signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-00.html#section-6.1.3 +func Verify(pub *PublicKey, message, sig []byte) bool { + deserializedSig, err := sphincs.DeserializeSignature(pub.Parameters, sig) + if err != nil { + return false + } + + return sphincs.Spx_verify(pub.Parameters, message, deserializedSig, pub.PublicData) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if subtle.ConstantTimeCompare(priv.PublicData.PKseed, priv.SecretData.PKseed) == 0 || + subtle.ConstantTimeCompare(priv.PublicData.PKroot, priv.SecretData.PKroot) == 0 { + return errors.KeyInvalidError("sphincs_plus: invalid public key") + } + + return +} + +// GetParametersFromModeAndId returns the instance Parameters given a Mode and a ParameterSetID +func GetParametersFromModeAndId(mode Mode, param ParameterSetId) (*parameters.Parameters, error) { + switch mode { + case ModeSimpleSHA2: + switch param { + case Parameter1: + return parameters.MakeSphincsPlusSHA256128sSimple(false), nil + case Parameter2: + return parameters.MakeSphincsPlusSHA256128fSimple(false), nil + case Parameter3: + return parameters.MakeSphincsPlusSHA256192sSimple(false), nil + case Parameter4: + return parameters.MakeSphincsPlusSHA256192fSimple(false), nil + case Parameter5: + return parameters.MakeSphincsPlusSHA256256sSimple(false), nil + case Parameter6: + return parameters.MakeSphincsPlusSHA256256fSimple(false), nil + default: + return nil, goerrors.New("sphincs_plus: invalid sha2 parameter") + } + case ModeSimpleShake: + switch param { + case Parameter1: + return parameters.MakeSphincsPlusSHAKE256128sSimple(false), nil + case Parameter2: + return parameters.MakeSphincsPlusSHAKE256128fSimple(false), nil + case Parameter3: + return parameters.MakeSphincsPlusSHAKE256192sSimple(false), nil + case Parameter4: + return parameters.MakeSphincsPlusSHAKE256192fSimple(false), nil + case Parameter5: + return parameters.MakeSphincsPlusSHAKE256256sSimple(false), nil + case Parameter6: + return parameters.MakeSphincsPlusSHAKE256256fSimple(false), nil + default: + return nil, goerrors.New("sphincs_plus: invalid shake parameter") + } + default: + return nil, goerrors.New("sphincs_plus: invalid hash algorithm") + } +} diff --git a/openpgp/sphincs_plus/sphincs_test.go b/openpgp/sphincs_plus/sphincs_test.go new file mode 100644 index 00000000..8a0e237d --- /dev/null +++ b/openpgp/sphincs_plus/sphincs_test.go @@ -0,0 +1,124 @@ +// Package sphincs_plus_test tests the implementation of SPHINCS+ signatures, suitable for OpenPGP, experimental. +package sphincs_plus_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/sphincs_plus" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string] sphincs_plus.Mode{ + "SHA2-Simple": sphincs_plus.ModeSimpleSHA2, + "SHAKE-Simple": sphincs_plus.ModeSimpleShake, + } + + params := map[string] sphincs_plus.ParameterSetId { + "1": sphincs_plus.Parameter1, + "2": sphincs_plus.Parameter2, + "3": sphincs_plus.Parameter3, + "4": sphincs_plus.Parameter4, + "5": sphincs_plus.Parameter5, + "6": sphincs_plus.Parameter6, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + for paramName, param := range params { + t.Run(paramName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo, param) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo, param) + }) + } + }) + } +} + +func testvalidateAlgo(t *testing.T, mode sphincs_plus.Mode, param sphincs_plus.ParameterSetId) { + key := testGenerateKeyAlgo(t, mode, param) + if err := sphincs_plus.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Serialize + pkBin, err := key.SerializePublic() + if err != nil { + t.Fatalf("unable to serialize public key") + } + + skBin, err := key.SerializePrivate() + if err != nil { + t.Fatalf("unable to serialize private key") + } + + // Deserialize + if err = key.UnmarshalPublic(pkBin); err != nil { + t.Fatalf("unable to deserialize public key") + } + + if err = key.UnmarshalPrivate(skBin); err != nil { + t.Fatalf("unable to deserialize private key") + } + + if err := sphincs_plus.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Corrupt the root of the public key + key.PublicData.PKroot[1] ^= 1 + + if err := sphincs_plus.Validate(key); err == nil { + t.Fatalf("failed to detect invalid root in key") + } + + // Re-load the correct public key + if err = key.UnmarshalPublic(pkBin); err != nil { + t.Fatalf("unable to deserialize public key") + } + + if err = key.UnmarshalPrivate(skBin); err != nil { + t.Fatalf("unable to deserialize private key") + } + + if err := sphincs_plus.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Corrupt the seed of the public key + key.PublicData.PKseed[1] ^= 1 + + if err := sphincs_plus.Validate(key); err == nil { + t.Fatalf("failed to detect invalid seed in key") + } +} + +func testGenerateKeyAlgo(t *testing.T, mode sphincs_plus.Mode, param sphincs_plus.ParameterSetId) *sphincs_plus.PrivateKey { + priv, err := sphincs_plus.GenerateKey(rand.Reader, mode, param) + if err != nil { + t.Fatal(err) + } + + return priv +} + + +func testSignVerifyAlgo(t *testing.T, priv *sphincs_plus.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + sig, err := sphincs_plus.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := sphincs_plus.Verify(&priv.PublicKey, digest, sig) + if !result { + t.Error("unable to verify message") + } +}