Skip to content

Commit

Permalink
feat(pqc): Add SLH-DSA pqc signing algorithm
Browse files Browse the repository at this point in the history
Implements SLH-DSA with circl according to:
https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html
  • Loading branch information
lubux committed Jan 20, 2025
1 parent 59cb1c8 commit 65ef280
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 21 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ require (
)

require golang.org/x/sys v0.22.0 // indirect

replace github.com/cloudflare/circl v1.5.0 => github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93 h1:lLX4wx3iE1uDt6v7pjcl2P8z4xTFHsp/1wOTRO+NPfg=
github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
Expand Down
14 changes: 8 additions & 6 deletions openpgp/integration_tests/v2/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ func generateFreshTestVectors(num int) (vectors []testVector, err error) {
v = "v6"
}
pkAlgoNames := map[packet.PublicKeyAlgorithm]string{
packet.PubKeyAlgoRSA: "rsa_" + v,
packet.PubKeyAlgoEdDSA: "EdDSA_" + v,
packet.PubKeyAlgoEd25519: "ed25519_" + v,
packet.PubKeyAlgoEd448: "ed448_" + v,
packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v,
packet.PubKeyAlgoRSA: "rsa_" + v,
packet.PubKeyAlgoEdDSA: "EdDSA_" + v,
packet.PubKeyAlgoEd25519: "ed25519_" + v,
packet.PubKeyAlgoEd448: "ed448_" + v,
packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v,
packet.PubKeyAlgoSlhDsaShake128s: "slhdsa128s_" + v,
}

newVector := testVector{
Expand Down Expand Up @@ -240,6 +241,7 @@ func randConfig() *packet.Config {
packet.PubKeyAlgoEd25519,
packet.PubKeyAlgoEd448,
packet.PubKeyAlgoMldsa65Ed25519,
packet.PubKeyAlgoSlhDsaShake128s,
}
pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))]

Expand Down Expand Up @@ -270,7 +272,7 @@ func randConfig() *packet.Config {
compConf := &packet.CompressionConfig{Level: level}

var v6 bool
if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 {
if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 || pkAlgo == packet.PubKeyAlgoSlhDsaShake128s {
v6 = true
} else if mathrand.Int()%2 == 0 {
v6 = true
Expand Down
14 changes: 13 additions & 1 deletion openpgp/key_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/slhdsa"
"github.com/ProtonMail/go-crypto/openpgp/symmetric"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
Expand Down Expand Up @@ -340,6 +341,17 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
}

return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d)
case packet.PubKeyAlgoSlhDsaShake128s, packet.PubKeyAlgoSlhDsaShake128f, packet.PubKeyAlgoSlhDsaShake256s:
if !config.V6() {
return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSH key")
}

scheme, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm())
if err != nil {
return nil, err
}

return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), scheme)
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
Expand Down Expand Up @@ -386,7 +398,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
case packet.ExperimentalPubKeyAlgoAEAD:
cipher := algorithm.CipherFunction(config.Cipher())
return symmetric.AEADGenerateKey(config.Random(), cipher)
case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448:
case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoSlhDsaShake128s, packet.PubKeyAlgoSlhDsaShake128f, packet.PubKeyAlgoSlhDsaShake256s:
if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil {
return nil, err
}
Expand Down
13 changes: 8 additions & 5 deletions openpgp/packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,11 @@ const (
PubKeyAlgoMlkem1024X448 = 106

// Experimental PQC DSA algorithms
PubKeyAlgoMldsa65Ed25519 = 107
PubKeyAlgoMldsa87Ed448 = 108
PubKeyAlgoMldsa65Ed25519 = 107
PubKeyAlgoMldsa87Ed448 = 108
PubKeyAlgoSlhDsaShake128s = 109
PubKeyAlgoSlhDsaShake128f = 110
PubKeyAlgoSlhDsaShake256s = 111
)

// CanEncrypt returns true if it's possible to encrypt a message to a public
Expand All @@ -539,7 +542,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool {
func (pka PublicKeyAlgorithm) CanSign() bool {
switch pka {
case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519,
PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448:
PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
return true
}
return false
Expand All @@ -549,9 +552,9 @@ func (pka PublicKeyAlgorithm) CanSign() bool {
// otherwise, it returns the selectedHash.
func (pka PublicKeyAlgorithm) HandleSpecificHash(selectedHash crypto.Hash) crypto.Hash {
switch pka {
case PubKeyAlgoMldsa65Ed25519:
case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f:
return crypto.SHA3_256
case PubKeyAlgoMldsa87Ed448:
case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhDsaShake256s:
return crypto.SHA3_512
}
return selectedHash
Expand Down
40 changes: 40 additions & 0 deletions openpgp/packet/private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
"github.com/ProtonMail/go-crypto/openpgp/slhdsa"
"github.com/ProtonMail/go-crypto/openpgp/symmetric"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
Expand Down Expand Up @@ -174,6 +175,8 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey
pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey)
case *mldsa_eddsa.PrivateKey:
pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey)
case *slhdsa.PrivateKey:
pk.PublicKey = *NewSlhdsaPublicKey(creationTime, &pubkey.PublicKey)
default:
panic("openpgp: unknown signer type in NewSignerPrivateKey")
}
Expand Down Expand Up @@ -582,6 +585,18 @@ func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) er
return nil
}

// serializeSlhDsaPrivateKey serializes a SLH-DSA private key.
func serializeSlhDsaPrivateKey(w io.Writer, priv *slhdsa.PrivateKey) error {
marshalledKey, err := priv.SecretSlhDsa.MarshalBinary()
if err != nil {
return err
}
if _, err := w.Write(marshalledKey); err != nil {
return err
}
return nil
}

// decrypt decrypts an encrypted private key using a decryption key.
func (pk *PrivateKey) decrypt(decryptionKey []byte) error {
if pk.Dummy() {
Expand Down Expand Up @@ -890,6 +905,8 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) {
err = serializeMlkemPrivateKey(w, priv)
case *mldsa_eddsa.PrivateKey:
err = serializeMldsaEddsaPrivateKey(w, priv)
case *slhdsa.PrivateKey:
err = serializeSlhDsaPrivateKey(w, priv)
default:
err = errors.InvalidArgumentError("unknown private key type")
}
Expand Down Expand Up @@ -930,6 +947,8 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) {
return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa_eddsa.MlDsaSeedLen)
case PubKeyAlgoMldsa87Ed448:
return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa_eddsa.MlDsaSeedLen)
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
return pk.parseSlhdsaPrivateKey(data)
default:
err = errors.StructuralError("unknown private key type")
return
Expand Down Expand Up @@ -1319,6 +1338,27 @@ func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, seedLen int)
return nil
}

// parseSlhdsaPrivateKey parses a SLH-DSA private key.
func (pk *PrivateKey) parseSlhdsaPrivateKey(data []byte) (err error) {
if pk.Version != 6 {
return goerrors.New("openpgp: cannot parse non-v6 SLH-DSA key")
}
parsedPublicKey := pk.PublicKey.PublicKey.(*slhdsa.PublicKey)
parsedPrivateKey := new(slhdsa.PrivateKey)
parsedPrivateKey.PublicKey = *parsedPublicKey
parsedPrivateKey.SecretSlhDsa, err = parsedPrivateKey.SlhDsa.UnmarshalBinaryPrivateKey(data)
if err != nil {
return goerrors.New("openpgp: failed to unmarshal SLH-DSA key")
}

if err := slhdsa.Validate(parsedPrivateKey); err != nil {
return err
}
pk.PrivateKey = parsedPrivateKey

return nil
}

func validateDSAParameters(priv *dsa.PrivateKey) error {
p := priv.P // group prime
q := priv.Q // subgroup order
Expand Down
84 changes: 81 additions & 3 deletions openpgp/packet/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/internal/encoding"
"github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa"
"github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh"
"github.com/ProtonMail/go-crypto/openpgp/slhdsa"
"github.com/ProtonMail/go-crypto/openpgp/symmetric"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
Expand All @@ -42,6 +43,7 @@ import (
"github.com/cloudflare/circl/sign"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
slhdsaCircl "github.com/cloudflare/circl/sign/slhdsa"
)

// PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2.
Expand Down Expand Up @@ -321,6 +323,23 @@ func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey)
return pk
}

func NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKey {
publicKeyBytes, err := pub.PublicSlhDsa.MarshalBinary()
if err != nil {
panic(err)
}
pk := &PublicKey{
Version: 6,
CreationTime: creationTime,
PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId),
PublicKey: pub,
q: encoding.NewOctetArray(publicKeyBytes),
}

pk.setFingerprintAndKeyId()
return pk
}

func (pk *PublicKey) parse(r io.Reader) (err error) {
// RFC 4880, section 5.5.2
var buf [6]byte
Expand Down Expand Up @@ -383,6 +402,8 @@ func (pk *PublicKey) parse(r io.Reader) (err error) {
err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize)
case PubKeyAlgoMldsa87Ed448:
err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize)
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
err = pk.parseSlhDsa(r)
default:
err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo)))
}
Expand Down Expand Up @@ -833,6 +854,29 @@ func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) {
return
}

func (pk *PublicKey) parseSlhDsa(r io.Reader) (err error) {
parsedPublicKey := &slhdsa.PublicKey{
AlgId: uint8(pk.PubKeyAlgo),
}

if parsedPublicKey.SlhDsa, err = GetSlhdsaSchemeFromAlgID(pk.PubKeyAlgo); err != nil {
return err
}

keyLen := parsedPublicKey.SlhDsa.PublicKeySize()
pk.q = encoding.NewEmptyOctetArray(keyLen)
if _, err = pk.q.ReadFrom(r); err != nil {
return
}

if parsedPublicKey.PublicSlhDsa, err = parsedPublicKey.SlhDsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil {
return err
}

pk.PublicKey = parsedPublicKey
return
}

// SerializeForHash serializes the PublicKey to w with the special packet
// header format needed for hashing.
func (pk *PublicKey) SerializeForHash(w io.Writer) error {
Expand Down Expand Up @@ -927,6 +971,8 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 {
PubKeyAlgoMldsa87Ed448:
length += uint32(pk.p.EncodedLength())
length += uint32(pk.q.EncodedLength())
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
length += uint32(pk.q.EncodedLength())
default:
panic("unknown public key algorithm")
}
Expand Down Expand Up @@ -1042,6 +1088,9 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) {
}
_, err = w.Write(pk.q.EncodedBytes())
return
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
_, err = w.Write(pk.q.EncodedBytes())
return
}
return errors.InvalidArgumentError("bad public-key algorithm")
}
Expand Down Expand Up @@ -1151,6 +1200,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro
return errors.SignatureError("MldsaEddsa verification failure")
}
return nil
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
if (pk.PubKeyAlgo == PubKeyAlgoSlhDsaShake128s || pk.PubKeyAlgo == PubKeyAlgoSlhDsaShake128f) && sig.Hash != crypto.SHA3_256 {
return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake128 requires sha3-256 message hash: has %s", sig.Hash))
}
if pk.PubKeyAlgo == PubKeyAlgoSlhDsaShake256s && sig.Hash != crypto.SHA3_512 {
return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash))
}
slhDsaPublicKey := pk.PublicKey.(*slhdsa.PublicKey)
if !slhdsa.Verify(slhDsaPublicKey, hashBytes, sig.SlhdsaSig.Bytes()) {
return errors.SignatureError("MldsaEddsa verification failure")
}
return nil
default:
return errors.SignatureError("Unsupported public key algorithm used in signature")
}
Expand Down Expand Up @@ -1384,6 +1445,8 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) {
case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519,
PubKeyAlgoMldsa87Ed448:
bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense.
case PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
bitLength = pk.q.BitLength()
default:
err = errors.InvalidArgumentError("bad public-key algorithm")
}
Expand Down Expand Up @@ -1427,7 +1490,8 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool {
func (pg *PublicKey) IsPQ() bool {
switch pg.PubKeyAlgo {
case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448,
PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448:
PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhDsaShake128s,
PubKeyAlgoSlhDsaShake128f, PubKeyAlgoSlhDsaShake256s:
return true
default:
return false
Expand All @@ -1436,9 +1500,9 @@ func (pg *PublicKey) IsPQ() bool {

func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) {
switch algId {
case PubKeyAlgoMldsa65Ed25519:
case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhDsaShake128s, PubKeyAlgoSlhDsaShake128f:
return PubKeyAlgoMlkem768X25519, nil
case PubKeyAlgoMldsa87Ed448:
case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhDsaShake256s:
return PubKeyAlgoMlkem1024X448, nil
default:
return 0, goerrors.New("packet: unsupported pq public key algorithm")
Expand All @@ -1457,6 +1521,20 @@ func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) {
}
}

// GetSlhdsaSchemeFromAlgID returns the SLH-DSA instance from the matching KEM
func GetSlhdsaSchemeFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) {
switch algId {
case PubKeyAlgoSlhDsaShake128s:
return slhdsaCircl.ParamIDSHAKESmall128, nil
case PubKeyAlgoSlhDsaShake128f:
return slhdsaCircl.ParamIDSHAKEFast128, nil
case PubKeyAlgoSlhDsaShake256s:
return slhdsaCircl.ParamIDSHAKESmall256, nil
default:
return nil, goerrors.New("packet: unsupported SLH-DSA public key algorithm")
}
}

// GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM
func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) {
switch algId {
Expand Down
Loading

0 comments on commit 65ef280

Please sign in to comment.