From ef9e903fca8712e2ce97463a643a0d72277898d3 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Mon, 20 Jan 2025 14:12:56 +0100 Subject: [PATCH] feat(pqc): Add SLH-DSA pqc signing algorithm Implements SLH-DSA with circl according to: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html --- go.mod | 2 + go.sum | 4 +- openpgp/integration_tests/v2/utils_test.go | 14 +- openpgp/key_generation.go | 14 +- openpgp/packet/packet.go | 14 +- openpgp/packet/private_key.go | 40 +++ openpgp/packet/public_key.go | 84 ++++- openpgp/packet/signature.go | 39 ++- openpgp/slhdsa/slhdsa.go | 73 +++++ openpgp/v2/key_generation.go | 15 +- openpgp/v2/read_test.go | 14 + openpgp/v2/read_write_test_data.go | 361 +++++++++++++++++++++ openpgp/v2/write.go | 4 +- 13 files changed, 657 insertions(+), 21 deletions(-) create mode 100644 openpgp/slhdsa/slhdsa.go diff --git a/go.mod b/go.mod index 25880a800..e59aeab28 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1a97c0f33..dad91470d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index ef9c18bff..bbb7bf003 100644 --- a/openpgp/integration_tests/v2/utils_test.go +++ b/openpgp/integration_tests/v2/utils_test.go @@ -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{ @@ -240,6 +241,7 @@ func randConfig() *packet.Config { packet.PubKeyAlgoEd25519, packet.PubKeyAlgoEd448, packet.PubKeyAlgoMldsa65Ed25519, + packet.PubKeyAlgoSlhdsaShake128s, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -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 diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index df4fab454..1b1387f06 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -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" @@ -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") } @@ -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 } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index f65efb2e5..2d68a82e6 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -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 @@ -539,7 +542,8 @@ 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 @@ -549,9 +553,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 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index aa0bfd61d..05fd909ce 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -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" @@ -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") } @@ -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() { @@ -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") } @@ -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 @@ -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 diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index b96469f07..56dd0958c 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -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" @@ -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. @@ -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 @@ -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))) } @@ -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 err + } + + if parsedPublicKey.PublicSlhdsa, err = parsedPublicKey.Slhdsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = parsedPublicKey + return nil +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -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") } @@ -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") } @@ -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") } @@ -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") } @@ -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 @@ -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") @@ -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 { diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 6c1ae22dc..7904980a6 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -25,6 +25,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) @@ -207,7 +208,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, - PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, + PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -358,6 +360,10 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { return } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if err = sig.parseSlhdsaSignature(r, sig.PubKeyAlgo); err != nil { + return + } default: panic("unreachable") } @@ -377,6 +383,17 @@ func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (er return } +// parseSlhdsaSignature parses an SLH-DSA signature as specified in +func (sig *Signature) parseSlhdsaSignature(r io.Reader, algID PublicKeyAlgorithm) (err error) { + scheme, err := GetSlhdsaSchemeFromAlgID(algID) + if err != nil { + return err + } + sig.SlhdsaSig = encoding.NewEmptyOctetArray(scheme.SignatureSize()) + _, err = sig.SlhdsaSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 9580, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -1056,6 +1073,22 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sig.MldsaSig = encoding.NewOctetArray(dSig) sig.EdDSASigR = encoding.NewOctetArray(ecSig) } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if sig.Version != 6 { + return errors.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + if (priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake128s || priv.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 priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake256s && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash)) + } + sk := priv.PrivateKey.(*slhdsa.PrivateKey) + dSig, err := slhdsa.Sign(sk, digest) + + if err == nil { + sig.SlhdsaSig = encoding.NewOctetArray(dSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1199,6 +1232,8 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: sigLength = int(sig.EdDSASigR.EncodedLength()) sigLength += int(sig.MldsaSig.EncodedLength()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + sigLength += int(sig.SlhdsaSig.EncodedLength()) default: panic("impossible") } @@ -1312,6 +1347,8 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.MldsaSig.EncodedBytes()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + _, err = w.Write(sig.SlhdsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/slhdsa/slhdsa.go b/openpgp/slhdsa/slhdsa.go new file mode 100644 index 000000000..4aee70491 --- /dev/null +++ b/openpgp/slhdsa/slhdsa.go @@ -0,0 +1,73 @@ +// Package slhdsa implements SLH-DSA-SHAKE, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html +package slhdsa + +import ( + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/slhdsa" +) + +type PublicKey struct { + AlgId uint8 + Slhdsa sign.Scheme + PublicSlhdsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretSlhdsa sign.PrivateKey +} + +// GenerateKey generates a SLH-DSA key. +func GenerateKey(rand io.Reader, algId uint8, scheme sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Slhdsa = scheme + + keySeed := make([]byte, scheme.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + priv.PublicKey.PublicSlhdsa, priv.SecretSlhdsa = priv.PublicKey.Slhdsa.DeriveKey(keySeed) + + return priv, nil +} + +// Sign generates a SLH-DSA signature. +func Sign(priv *PrivateKey, message []byte) (signature []byte, err error) { + // The specification of SLH-DSA [FIPS-205] prescribes an optional non-deterministic message randomizer. + // This is not used in this specification + options := slhdsa.SignatureOpts{ + PreHashID: slhdsa.NoPreHash, + IsDeterministic: true, + } + signature, err = priv.SecretSlhdsa.Sign(nil, message, &options) + if err != nil { + return nil, fmt.Errorf("slhdsa: unable to sign with SLH-DSA: %s", err) + } + if signature == nil { + return nil, goerrors.New("slhdsa: unable to sign with SLH-DSA") + } + + return signature, nil +} + +// Verify verifies the SLH-DSA signature. +func Verify(pub *PublicKey, message, dSig []byte) bool { + return pub.Slhdsa.Verify(pub.PublicSlhdsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if !priv.PublicSlhdsa.Equal(priv.SecretSlhdsa.Public()) { + return errors.KeyInvalidError("slhdsa: invalid public key") + } + + return nil +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 84d328c22..1ac8a841c 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -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" @@ -420,6 +421,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") + } + + d, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -466,7 +478,8 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey 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 } diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 08607667b..acc26cd58 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -1158,3 +1159,16 @@ func TestPqcDraftVectors(t *testing.T) { }) } } + +func TestPqcDraftKey(t *testing.T) { + t.Skip("skipping") + secretKey, err := ReadArmoredKeyRing(strings.NewReader(v6SlhDsaMlkem768PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + _, ok := secretKey[0].EncryptionKey(time.Unix(1737373639, 0), nil) + if !ok { + t.Fatal("Failed to verify key") + } +} diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 2e8a68680..2f09991db 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -979,3 +979,364 @@ rD9h0SH7PihV9SRdvR2vvWyn7ygFNPajy/8PTH15eEv/5g6ZWxs5CKvpz0hTqf8C 0lQCCQIMslhjNg7KUOTtedOwUxvAoHK/lZf4fpMbG2GW7r6OHwShQ/zNruQmR8qV qJsN7xv8+utysXtt6SUgMPnF3oUp9HzBnCwHb/m/di69xNsYQAE= -----END PGP MESSAGE-----` + +const v6SlhDsaMlkem768PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +GXjAz110OD7IrJykydmxG+WiVkeP7RdkMCXM5OGNJ/4aXGuTyLCMRjk4gSp6 +V+jYMc28I5EDWfTQagLFeF8d3VTpR38eZxrvj9/jbVc46wXW2ZfCVbj9l2C8 +bybOKpmtNJEFxA+Ya2AcCKtqRyFHfeDD3fNMgtn/PbP7MeOcEVEaBrl7YQQN +V/fx7tskWXF1/xmZVhYmNOrNf854GW3OunSUV8FgMyRGq4dS5W79cGhQBQ23 +sPsZj6Tof88zDbesv8+E4dqPnFlynn4BF9CIB+GJk6v9tSsNsipgGlxSNw4k +wN7Ar0kpRhnbNMk7BGj/IIAbavzMa7rEcHxKeJnCaS6YDjzr8n5WJYC5/7j9 +gvaVNbL61eZ4rEVrE1l4am1uxARHaN+UjrndnzAztyAOHzukTnAnhpRoq2x4 +eHffMeoTIc7xWIGgxg0MENyAas0FR7npxwtU7PaEkLV4NYEK1sTQ5LfccVUV +PXPgkqVVqFMlYPTpJ3zXSe4VewWmqrx7AWjPMNCyW6Va3O3Lq/oUz/5jDfGv +y7EHITH9Oe0avRDyoqfv8Waj4ps1Yb2eGgDDDCnbqlbpXKlBp6WIoIy+2Fs9 +EcYbhCt9vuYknFyD8WN4+xpbc0lPODocRoQqmmhfYedPJ6K5+nmzYRHf2Mn7 +wAp4DFB9O+zc6QFmztc5wab/PB7oIE5BL3fe5nxmne6iIPQhJVD4m/Bo6Qhb ++Guh3LhM9N2dwZnEEpwaT1P27LUuersi53ktc1mac2djj+E13JQCoex/83wa +qowIYHfkFO87dY3wR7ZmYRCUb+72Kq6AGVx3WGDk2IgGmEC4b9DeUxU2ipQl +Skn9+oI18lxjG0gMSgwYeqo80+JDF9dyheztuUUyeP1ArQKDzFnDj/K0WcDg +ELrYi0gp1lTCc5WeBH0/9O35SvJybfqRp/hQHl79vzlbHissGRidyS5doxKu +OyTwypC26m0Sr/O020/v/h3QsITFP5dShRLduE8tV3C/SrtUuvGjqWgQAHbR +kYKlU+YLBa3Dj+LqgeUwW/u2Hj5aiEBUq8zg7TAverEHQ+csbDLyHlLbSDPd +whWaNjAONWzmSUEehjnn6ujQORdtpqLDBqpfTQBbShXZBMMpE4h8G5rbPFLM +sxrNpSx42F6qkVaVULsiEaTbIBYVnrbdK258HKpg2Qydzti1lx4N16unTxIh +T45cEAJK5Jg0OkEihHQEMhzok5WWsKbQs7hyhw6CJv7xpSO7Sh6pvB3Y1Djg +qwBbad3i7yRO9RI2cYnUzjzsstcqj5JPcbL78blZJ4Jv6e9ewMT74JG3bV02 +7Y+XXJm/QpW2mXxdvnC5iu2tQaKPju5JYPa/qyF00/r45994Foi+hcAEyK5O +Tl7ic9aPNHlD57EKuHdMRxvihqJvt+IoHlNJ6EvD71wUpDTyh9nRgSMS7MrM +1gC0MhO4cO2biYLMePGUpmLyK4p9ajKtTqqIZFfL7eJVTHiJEmumW5vJIoYw +GoPOj8mz8as11N2ny50Ys4pv7qhNfkEsGqHI9iiBjojXiiG4Cyi8hVGgnmfT +cWd3pEpUuiQMWBKokikyM/gYk90i01QY7caO95/zWcfKT7uBQime0krKeUaD +LY5p1KmSY/wj7acbs8PYJuW7kFQOKmP33kMEcmJ4VO1rc8F5jarBICCut56L ++H95aBsnt7ObJ5rdZMfgvfnD6sRVV7mLsXqQ4OSYhQsDPqjXgE/rlnGGjiHM +anDBIZPOFBUIlK+mmIvXahyKE6nflbRe4gcNFq5AtFz5E3yMvVKJ42eKck21 +EJwlIDlxOkc9/t8hUngoY3V41sU/Y535FkcBxc3oN9LRnAU8DN7WNIBlAlPB +edOw+lU8HZRlsOuHRQLQ2eboJSR+T890GoaIH/NgPWR9f22E3nm2qcFTd6RV +FMhyLj/uhUlFCdKxlF0aLeyMa40rO/U4yUXcMpnNMTRw7soungOjvy7Frhfb +m9ry4yST5pCNNzsgvCpJmc8hhCfFyNzp6KqBbBamz0hLMcgBQ5kZlOLcbPIM +jbOZ0iVrRojMjhwSD+WNxJNo70cCz96GOnhoXEcMJyxIDKm5s4oR/hqgoDmi +53MgZKWc8qWo8pVy7iqwRRamaXB9DLvovo+PWlSxm91i66JgcoaAyAlnY/U+ +W7aE +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 8509f6fe9..6144153a9 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -1057,11 +1057,11 @@ func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { } } } - case packet.PubKeyAlgoMldsa65Ed25519: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f: return []uint8{ hashToHashId(crypto.SHA3_256), } - case packet.PubKeyAlgoMldsa87Ed448: + case packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoSlhdsaShake256s: return []uint8{ hashToHashId(crypto.SHA3_512), }