diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e7c72e84d..d26868aa2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [ main ] + branches: [ main, Proton ] pull_request: - branches: [ main ] + branches: [ main, Proton ] jobs: diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go new file mode 100644 index 000000000..c05937654 --- /dev/null +++ b/openpgp/benchmark_v6_test.go @@ -0,0 +1,295 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + "io/ioutil" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const benchmarkMessageSize = 1024 // Signed / encrypted message size in bytes + +var benchmarkTestSet = map[string]*packet.Config{ + "RSA_1024": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 1024, + }, + "RSA_2048": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 2048, + }, + "RSA_3072": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 3072, + }, + "RSA_4096": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 4096, + }, + "Ed25519_X25519": { + Algorithm: packet.PubKeyAlgoEd25519, + }, + "Ed448_X448": { + Algorithm: packet.PubKeyAlgoEd448, + }, + "P256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP256, + }, + "P384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP384, + }, + "P521": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP521, + }, + "Brainpool256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP256, + }, + "Brainpool384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP384, + }, + "Brainpool512": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP512, + }, + "ML-DSA3Ed25519_ML-KEM768X25519": { + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + }, + "ML-DSA5Ed448_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + }, +} + +func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { + var serializedEntities [][]byte + config.V6Keys = true + + config.AEADConfig = &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + } + + config.Time = func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + b.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + b.Fatalf("Failed to serialize entity: %s", err) + } + + serializedEntities = append(serializedEntities, serializedEntity.Bytes()) + } + + return serializedEntities +} + +func benchmarkParse(b *testing.B, keys [][]byte) []*Entity { + var parsedKeys []*Entity + + b.ResetTimer() + for n := 0; n < b.N; n++ { + keyring, err := ReadKeyRing(bytes.NewReader(keys[n])) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + parsedKeys = append(parsedKeys, keyring[0]) + } + + return parsedKeys +} + +func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) [][]byte { + var encryptedMessages [][]byte + + var config = &packet.Config{ + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + V6Keys: true, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + var signed *Entity + if sign { + signed = keys[n%len(keys)] + } + + w, err := Encrypt(buf, EntityList{keys[n%len(keys)]}, signed, nil, config) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + _, err = w.Write(plaintext) + if err != nil { + b.Errorf("Error writing plaintext: %s", err) + continue + } + + err = w.Close() + if err != nil { + b.Errorf("Error closing WriteCloser: %s", err) + continue + } + + encryptedMessages = append(encryptedMessages, buf.Bytes()) + } + + return encryptedMessages +} + +func benchmarkDecrypt(b *testing.B, keys []*Entity, plaintext []byte, encryptedMessages [][]byte, verify bool) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + reader := bytes.NewReader(encryptedMessages[n%len(encryptedMessages)]) + md, err := ReadMessage(reader, EntityList{keys[n%len(keys)]}, nil, nil) + if err != nil { + b.Errorf("Error reading message: %s", err) + continue + } + + decrypted, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + b.Errorf("Error reading encrypted content: %s", err) + continue + } + + if !bytes.Equal(decrypted, plaintext) { + b.Error("Decrypted wrong plaintext") + } + + if verify { + if md.SignatureError != nil { + b.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + b.Error("Signature missing") + } + } + } +} + +func benchmarkSign(b *testing.B, keys []*Entity, plaintext []byte) [][]byte { + var signatures [][]byte + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + err := DetachSign(buf, keys[n%len(keys)], bytes.NewReader(plaintext), nil) + if err != nil { + b.Errorf("Failed to sign: %s", err) + continue + } + + signatures = append(signatures, buf.Bytes()) + } + + return signatures +} + +func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures [][]byte) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + signed := bytes.NewReader(plaintext) + signature := bytes.NewReader(signatures[n%len(signatures)]) + + parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n%len(keys)]}, signed, signature, nil) + + if signatureError != nil { + b.Errorf("Signature error: %s", signatureError) + } + + if parsedSignature == nil { + b.Error("Signature missing") + } + + if signer == nil { + b.Error("Signer missing") + } + } +} + +func BenchmarkV6Keys(b *testing.B) { + serializedKeys := make(map[string][][]byte) + parsedKeys := make(map[string][]*Entity) + encryptedMessages := make(map[string][][]byte) + encryptedSignedMessages := make(map[string][][]byte) + signatures := make(map[string][][]byte) + + var plaintext [benchmarkMessageSize]byte + _, _ = rand.Read(plaintext[:]) + + for name, config := range benchmarkTestSet { + b.Run("Generate "+name, func(b *testing.B) { + serializedKeys[name] = benchmarkGenerateKey(b, config) + b.Logf("Generate %s: %d bytes", name, len(serializedKeys[name][0])) + }) + } + + for name, keys := range serializedKeys { + b.Run("Parse_"+name, func(b *testing.B) { + parsedKeys[name] = benchmarkParse(b, keys) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_"+name, func(b *testing.B) { + encryptedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], false) + b.Logf("Encrypt %s: %d bytes", name, len(encryptedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedMessages[name], false) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_Sign_"+name, func(b *testing.B) { + encryptedSignedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], true) + b.Logf("Encrypt_Sign %s: %d bytes", name, len(encryptedSignedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_Verify_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedSignedMessages[name], true) + }) + } + + for name, keys := range parsedKeys { + b.Run("Sign_"+name, func(b *testing.B) { + signatures[name] = benchmarkSign(b, keys, plaintext[:]) + b.Logf("Sign %s: %d bytes", name, len(signatures[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Verify_"+name, func(b *testing.B) { + benchmarkVerify(b, keys, plaintext[:], signatures[name]) + }) + } +} diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index 460347afb..5dd8b98c6 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -415,7 +415,7 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config if sk.Version == 6 { // generate salt var salt []byte - salt, err = packet.SignatureSaltForHash(hashType, config.Random()) + salt, err = packet.SignatureSaltForHash(selectedHashType, config.Random()) if err != nil { return } @@ -533,7 +533,7 @@ func nameOfHash(h crypto.Hash) string { func acceptableHashesToWrite(singingKey *packet.PublicKey) []crypto.Hash { switch singingKey.PubKeyAlgo { - case packet.PubKeyAlgoEd448: + case packet.PubKeyAlgoEd448, packet.PubKeyAlgoMldsa87Ed448: return []crypto.Hash{ crypto.SHA512, crypto.SHA3_512, diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index db8fb163b..85a06b17c 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -12,13 +12,50 @@ import ( "io" "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519" +) + +const ( + KDFVersion1 = 1 + KDFVersionForwarding = 255 ) type KDF struct { - Hash algorithm.Hash - Cipher algorithm.Cipher + Version int // Defaults to v1; 255 for forwarding + Hash algorithm.Hash + Cipher algorithm.Cipher + ReplacementFingerprint []byte // (forwarding only) fingerprint to use instead of recipient's (20 octets) +} + +func (kdf *KDF) Serialize(w io.Writer) (err error) { + switch kdf.Version { + case 0, KDFVersion1: // Default to v1 if unspecified + return kdf.serializeForHash(w) + case KDFVersionForwarding: + // Length || Version || Hash || Cipher || Replacement Fingerprint + length := byte(3 + len(kdf.ReplacementFingerprint)) + if _, err := w.Write([]byte{length, KDFVersionForwarding, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + if _, err := w.Write(kdf.ReplacementFingerprint); err != nil { + return err + } + + return nil + default: + return errors.New("ecdh: invalid KDF version") + } +} + +func (kdf *KDF) serializeForHash(w io.Writer) (err error) { + // Length || Version || Hash || Cipher + if _, err := w.Write([]byte{3, KDFVersion1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + return nil } type PublicKey struct { @@ -32,13 +69,10 @@ type PrivateKey struct { D []byte } -func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey { +func NewPublicKey(curve ecc.ECDHCurve, kdf KDF) *PublicKey { return &PublicKey{ curve: curve, - KDF: KDF{ - Hash: kdfHash, - Cipher: kdfCipher, - }, + KDF: kdf, } } @@ -149,21 +183,31 @@ func Decrypt(priv *PrivateKey, vsG, c, curveOID, fingerprint []byte) (msg []byte } func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLeading, stripTrailing bool) ([]byte, error) { - // Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 - // || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap + // Param = curve_OID_len || curve_OID || public_key_alg_ID + // || KDF_params for AESKeyWrap // || "Anonymous Sender " || recipient_fingerprint; param := new(bytes.Buffer) if _, err := param.Write(curveOID); err != nil { return nil, err } - algKDF := []byte{18, 3, 1, pub.KDF.Hash.Id(), pub.KDF.Cipher.Id()} - if _, err := param.Write(algKDF); err != nil { + algo := []byte{18} + if _, err := param.Write(algo); err != nil { + return nil, err + } + + if err := pub.KDF.serializeForHash(param); err != nil { return nil, err } + if _, err := param.Write([]byte("Anonymous Sender ")); err != nil { return nil, err } - if _, err := param.Write(fingerprint[:]); err != nil { + + if pub.KDF.ReplacementFingerprint != nil { + fingerprint = pub.KDF.ReplacementFingerprint + } + + if _, err := param.Write(fingerprint); err != nil { return nil, err } @@ -204,3 +248,40 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead func Validate(priv *PrivateKey) error { return priv.curve.ValidateECDH(priv.Point, priv.D) } + +func DeriveProxyParam(recipientKey, forwardeeKey *PrivateKey) (proxyParam []byte, err error) { + if recipientKey.GetCurve().GetCurveName() != "curve25519" { + return nil, pgperrors.InvalidArgumentError("recipient subkey is not curve25519") + } + + if forwardeeKey.GetCurve().GetCurveName() != "curve25519" { + return nil, pgperrors.InvalidArgumentError("forwardee subkey is not curve25519") + } + + c := ecc.NewCurve25519() + + // Clamp and reverse two secrets + proxyParam, err = curve25519.DeriveProxyParam(c.MarshalByteSecret(recipientKey.D), c.MarshalByteSecret(forwardeeKey.D)) + + return proxyParam, err +} + +func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) { + c := ecc.NewCurve25519() + + parsedEphemeral := c.UnmarshalBytePoint(ephemeral) + if parsedEphemeral == nil { + return nil, pgperrors.InvalidArgumentError("invalid ephemeral") + } + + if len(proxyParam) != curve25519.ParamSize { + return nil, pgperrors.InvalidArgumentError("invalid proxy parameter") + } + + transformed, err := curve25519.ProxyTransform(parsedEphemeral, proxyParam) + if err != nil { + return nil, err + } + + return c.MarshalBytePoint(transformed), nil +} diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 1f70b7dd0..0170d776c 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -88,7 +88,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { p := priv.MarshalPoint() d := priv.MarshalByteSecret() - parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF.Hash, priv.KDF.Cipher)) + parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF)) if err := parsed.UnmarshalPoint(p); err != nil { t.Fatalf("unable to unmarshal point: %s", err) @@ -112,3 +112,37 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { t.Fatal("failed to marshal/unmarshal correctly") } } + +func TestKDFParamsWrite(t *testing.T) { + kdf := KDF{ + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + } + byteBuffer := new(bytes.Buffer) + + testFingerprint := make([]byte, 20) + + expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()} + kdf.Serialize(byteBuffer) + gotBytes := byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV1) { + t.Errorf("error serializing KDF params, got %x, want: %x", gotBytes, expectBytesV1) + } + byteBuffer.Reset() + + kdfV2 := KDF{ + Version: KDFVersionForwarding, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + ReplacementFingerprint: testFingerprint, + } + expectBytesV2 := []byte{23, 0xFF, kdfV2.Hash.Id(), kdfV2.Cipher.Id()} + expectBytesV2 = append(expectBytesV2, testFingerprint...) + + kdfV2.Serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2) { + t.Errorf("error serializing KDF params v2, got %x, want: %x", gotBytes, expectBytesV2) + } + byteBuffer.Reset() +} diff --git a/openpgp/errors/errors.go b/openpgp/errors/errors.go index 2e341507a..d2d1e44b3 100644 --- a/openpgp/errors/errors.go +++ b/openpgp/errors/errors.go @@ -74,6 +74,8 @@ func (i InvalidArgumentError) Error() string { return "openpgp: invalid argument: " + string(i) } +var InvalidForwardeeKeyError = InvalidArgumentError("invalid forwardee key") + // SignatureError indicates that a syntactically valid signature failed to // validate. type SignatureError string diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go new file mode 100644 index 000000000..ae45c3c2b --- /dev/null +++ b/openpgp/forwarding.go @@ -0,0 +1,163 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openpgp + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + i := e.PrimaryIdentity() + if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired + i.SelfSignature.SigExpired(now) || // user ID self-signature has expired + e.Revoked(now) || // primary key has been revoked + i.Revoked(now) { // user ID has been revoked + return nil, nil, errors.InvalidArgumentError("primary key is expired") + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + keyLifetimeSecs := config.KeyLifetime() + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs, true) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + // Filter expiration & revokal + if forwarderSubKey.PublicKey.KeyExpired(forwarderSubKey.Sig, now) || + forwarderSubKey.Sig.SigExpired(now) || + forwarderSubKey.Revoked(now) { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKey.Sig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKey.Sig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKey.Sig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKey.Sig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKey.Sig.FlagForward = true + + // Re-sign subkey binding signature + err = forwardeeSubKey.Sig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go new file mode 100644 index 000000000..7bc167180 --- /dev/null +++ b/openpgp/forwarding_test.go @@ -0,0 +1,224 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "golang.org/x/crypto/openpgp/armor" +) + +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj +-----END PGP PRIVATE KEY BLOCK-----` + +const forwardedMessage = `-----BEGIN PGP MESSAGE----- + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa +-----END PGP MESSAGE-----` + +const forwardedPlaintext = "Message for Bob" + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) + if err != nil { + t.Error(err) + return + } + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) + if err != nil { + t.Error(err) + return + } + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) + if err != nil { + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingFull(t *testing.T) { + keyConfig := &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + Curve: packet.Curve25519, + } + + plaintext := make([]byte, 1024) + rand.Read(plaintext) + + bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig) + if err != nil { + t.Fatal(err) + } + + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) + } + + if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + _, err = w.Write(plaintext) + if err != nil { + t.Fatal(err) + } + + err = w.Close() + if err != nil { + t.Fatal(err) + } + + encrypted := buf.Bytes() + + // Decrypt message for Bob + m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("decrypted does not match original") + } + + // Forward message + + transformed := transformTestMessage(t, encrypted, instances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } + + // Setup further forwarding + danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { + bytesReader := bytes.NewReader(encrypted) + packets := packet.NewReader(bytesReader) + splitPoint := int64(0) + transformedEncryptedKey := bytes.NewBuffer(nil) + +Loop: + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error in parsing message: %s", err) + } + switch p := p.(type) { + case *packet.EncryptedKey: + tp, err := p.ProxyTransform(instance) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = tp.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + return transformed +} + +func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity { + serializedEntity := bytes.NewBuffer(nil) + err := key.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatalf("Error in serializing forwardee key: %s", err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatalf("Error in reading forwardee key: %s", err) + } + + if len(el) != 1 { + t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el)) + } + + return el[0] +} diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index 0c3c49c31..ef9c18bff 100644 --- a/openpgp/integration_tests/v2/utils_test.go +++ b/openpgp/integration_tests/v2/utils_test.go @@ -30,10 +30,11 @@ 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.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v, } newVector := testVector{ @@ -238,6 +239,7 @@ func randConfig() *packet.Config { packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoEd25519, packet.PubKeyAlgoEd448, + packet.PubKeyAlgoMldsa65Ed25519, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -268,7 +270,9 @@ func randConfig() *packet.Config { compConf := &packet.CompressionConfig{Level: level} var v6 bool - if mathrand.Int()%2 == 0 { + if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 { + v6 = true + } else if mathrand.Int()%2 == 0 { v6 = true if pkAlgo == packet.PubKeyAlgoEdDSA { pkAlgo = packet.PubKeyAlgoEd25519 diff --git a/openpgp/internal/algorithm/aead.go b/openpgp/internal/algorithm/aead.go index d06706518..02d26a862 100644 --- a/openpgp/internal/algorithm/aead.go +++ b/openpgp/internal/algorithm/aead.go @@ -12,6 +12,11 @@ import ( // operation. type AEADMode uint8 +// Id returns the algorithm ID, as a byte, of mode. +func (mode AEADMode) Id() uint8 { + return uint8(mode) +} + // Supported modes of operation (see RFC4880bis [EAX] and RFC7253) const ( AEADModeEAX = AEADMode(1) diff --git a/openpgp/internal/algorithm/cipher.go b/openpgp/internal/algorithm/cipher.go index c76a75bcd..df3e5396c 100644 --- a/openpgp/internal/algorithm/cipher.go +++ b/openpgp/internal/algorithm/cipher.go @@ -46,7 +46,7 @@ var CipherById = map[uint8]Cipher{ type CipherFunction uint8 -// ID returns the algorithm Id, as a byte, of cipher. +// Id returns the algorithm ID, as a byte, of cipher. func (sk CipherFunction) Id() uint8 { return uint8(sk) } diff --git a/openpgp/internal/ecc/curve25519.go b/openpgp/internal/ecc/curve25519.go index e047b3b3b..4bfd15b28 100644 --- a/openpgp/internal/ecc/curve25519.go +++ b/openpgp/internal/ecc/curve25519.go @@ -3,10 +3,9 @@ package ecc import ( "crypto/subtle" - "io" - "github.com/ProtonMail/go-crypto/openpgp/errors" x25519lib "github.com/cloudflare/circl/dh/x25519" + "io" ) type curve25519 struct{} diff --git a/openpgp/internal/ecc/curve25519/curve25519.go b/openpgp/internal/ecc/curve25519/curve25519.go new file mode 100644 index 000000000..21670a82c --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519.go @@ -0,0 +1,122 @@ +// Package curve25519 implements custom field operations without clamping for forwarding. +package curve25519 + +import ( + "crypto/subtle" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519/field" + x25519lib "github.com/cloudflare/circl/dh/x25519" + "math/big" +) + +var curveGroupByte = [x25519lib.Size]byte{ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, +} + +const ParamSize = x25519lib.Size + +func DeriveProxyParam(recipientSecretByte, forwardeeSecretByte []byte) (proxyParam []byte, err error) { + curveGroup := new(big.Int).SetBytes(curveGroupByte[:]) + recipientSecret := new(big.Int).SetBytes(recipientSecretByte) + forwardeeSecret := new(big.Int).SetBytes(forwardeeSecretByte) + + proxyTransform := new(big.Int).Mod( + new(big.Int).Mul( + new(big.Int).ModInverse(forwardeeSecret, curveGroup), + recipientSecret, + ), + curveGroup, + ) + + rawProxyParam := proxyTransform.Bytes() + + // pad and convert to small endian + proxyParam = make([]byte, x25519lib.Size) + l := len(rawProxyParam) + for i := 0; i < l; i++ { + proxyParam[i] = rawProxyParam[l-i-1] + } + + return proxyParam, nil +} + +func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) { + var transformed, safetyCheck [x25519lib.Size]byte + + var scalarEight = make([]byte, x25519lib.Size) + scalarEight[0] = 0x08 + err := ScalarMult(&safetyCheck, scalarEight, ephemeral) + if err != nil { + return nil, err + } + + err = ScalarMult(&transformed, proxyParam, ephemeral) + if err != nil { + return nil, err + } + + return transformed[:], nil +} + +func ScalarMult(dst *[32]byte, scalar, point []byte) error { + var in, base, zero [32]byte + copy(in[:], scalar) + copy(base[:], point) + + scalarMult(dst, &in, &base) + if subtle.ConstantTimeCompare(dst[:], zero[:]) == 1 { + return errors.InvalidArgumentError("invalid ephemeral: low order point") + } + + return nil +} + +func scalarMult(dst, scalar, point *[32]byte) { + var e [32]byte + + copy(e[:], scalar[:]) + + var x1, x2, z2, x3, z3, tmp0, tmp1 field.Element + x1.SetBytes(point[:]) + x2.One() + x3.Set(&x1) + z3.One() + + swap := 0 + for pos := 254; pos >= 0; pos-- { + b := e[pos/8] >> uint(pos&7) + b &= 1 + swap ^= int(b) + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + swap = int(b) + + tmp0.Subtract(&x3, &z3) + tmp1.Subtract(&x2, &z2) + x2.Add(&x2, &z2) + z2.Add(&x3, &z3) + z3.Multiply(&tmp0, &x2) + z2.Multiply(&z2, &tmp1) + tmp0.Square(&tmp1) + tmp1.Square(&x2) + x3.Add(&z3, &z2) + z2.Subtract(&z3, &z2) + x2.Multiply(&tmp1, &tmp0) + tmp1.Subtract(&tmp1, &tmp0) + z2.Square(&z2) + + z3.Mult32(&tmp1, 121666) + x3.Square(&x3) + tmp0.Add(&tmp0, &z3) + z3.Multiply(&x1, &z2) + z2.Multiply(&tmp1, &tmp0) + } + + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + + z2.Invert(&z2) + x2.Multiply(&x2, &z2) + copy(dst[:], x2.Bytes()) +} diff --git a/openpgp/internal/ecc/curve25519/curve25519_test.go b/openpgp/internal/ecc/curve25519/curve25519_test.go new file mode 100644 index 000000000..bd82e03e2 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519_test.go @@ -0,0 +1,89 @@ +// Package curve25519 implements custom field operations without clamping for forwarding. +package curve25519 + +import ( + "bytes" + "encoding/hex" + "testing" +) + +const ( + hexBobSecret = "5989216365053dcf9e35a04b2a1fc19b83328426be6bb7d0a2ae78105e2e3188" + hexCharlesSecret = "684da6225bcd44d880168fc5bec7d2f746217f014c8019005f144cc148f16a00" + hexExpectedProxyParam = "e89786987c3a3ec761a679bc372cd11a425eda72bd5265d78ad0f5f32ee64f02" + + hexMessagePoint = "aaea7b3bb92f5f545d023ccb15b50f84ba1bdd53be7f5cfadcfb0106859bf77e" + hexInputProxyParam = "83c57cbe645a132477af55d5020281305860201608e81a1de43ff83f245fb302" + hexExpectedTransformedPoint = "ec31bb937d7ef08c451d516be1d7976179aa7171eea598370661d1152b85005a" + + hexSmallSubgroupPoint = "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" +) + +func TestDeriveProxyParam(t *testing.T) { + bobSecret, err := hex.DecodeString(hexBobSecret) + if err != nil { + t.Fatalf("Unexpected error in decoding recipient secret: %s", err) + } + + charlesSecret, err := hex.DecodeString(hexCharlesSecret) + if err != nil { + t.Fatalf("Unexpected error in decoding forwardee secret: %s", err) + } + + expectedProxyParam, err := hex.DecodeString(hexExpectedProxyParam) + if err != nil { + t.Fatalf("Unexpected error in parameter decoding expected proxy parameter: %s", err) + } + + proxyParam, err := DeriveProxyParam(bobSecret, charlesSecret) + if err != nil { + t.Fatalf("Unexpected error in parameter derivation: %s", err) + } + + if bytes.Compare(proxyParam, expectedProxyParam) != 0 { + t.Errorf("Computed wrong proxy parameter, expected %x got %x", expectedProxyParam, proxyParam) + } +} + +func TestTransformMessage(t *testing.T) { + proxyParam, err := hex.DecodeString(hexInputProxyParam) + if err != nil { + t.Fatalf("Unexpected error in decoding proxy parameter: %s", err) + } + + messagePoint, err := hex.DecodeString(hexMessagePoint) + if err != nil { + t.Fatalf("Unexpected error in decoding message point: %s", err) + } + + expectedTransformed, err := hex.DecodeString(hexExpectedTransformedPoint) + if err != nil { + t.Fatalf("Unexpected error in parameter decoding expected transformed point: %s", err) + } + + transformed, err := ProxyTransform(messagePoint, proxyParam) + if err != nil { + t.Fatalf("Unexpected error in parameter derivation: %s", err) + } + + if bytes.Compare(transformed, expectedTransformed) != 0 { + t.Errorf("Computed wrong proxy parameter, expected %x got %x", expectedTransformed, transformed) + } +} + +func TestTransformSmallSubgroup(t *testing.T) { + proxyParam, err := hex.DecodeString(hexInputProxyParam) + if err != nil { + t.Fatalf("Unexpected error in decoding proxy parameter: %s", err) + } + + messagePoint, err := hex.DecodeString(hexSmallSubgroupPoint) + if err != nil { + t.Fatalf("Unexpected error in decoding small sugroup point: %s", err) + } + + _, err = ProxyTransform(messagePoint, proxyParam) + if err == nil { + t.Error("Expected small subgroup error") + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe.go b/openpgp/internal/ecc/curve25519/field/fe.go new file mode 100644 index 000000000..ca841ad99 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe.go @@ -0,0 +1,416 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package field implements fast arithmetic modulo 2^255-19. +package field + +import ( + "crypto/subtle" + "encoding/binary" + "math/bits" +) + +// Element represents an element of the field GF(2^255-19). Note that this +// is not a cryptographically secure group, and should only be used to interact +// with edwards25519.Point coordinates. +// +// This type works similarly to math/big.Int, and all arguments and receivers +// are allowed to alias. +// +// The zero value is a valid zero element. +type Element struct { + // An element t represents the integer + // t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204 + // + // Between operations, all limbs are expected to be lower than 2^52. + l0 uint64 + l1 uint64 + l2 uint64 + l3 uint64 + l4 uint64 +} + +const maskLow51Bits uint64 = (1 << 51) - 1 + +var feZero = &Element{0, 0, 0, 0, 0} + +// Zero sets v = 0, and returns v. +func (v *Element) Zero() *Element { + *v = *feZero + return v +} + +var feOne = &Element{1, 0, 0, 0, 0} + +// One sets v = 1, and returns v. +func (v *Element) One() *Element { + *v = *feOne + return v +} + +// reduce reduces v modulo 2^255 - 19 and returns it. +func (v *Element) reduce() *Element { + v.carryPropagate() + + // After the light reduction we now have a field element representation + // v < 2^255 + 2^13 * 19, but need v < 2^255 - 19. + + // If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1, + // generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise. + c := (v.l0 + 19) >> 51 + c = (v.l1 + c) >> 51 + c = (v.l2 + c) >> 51 + c = (v.l3 + c) >> 51 + c = (v.l4 + c) >> 51 + + // If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's + // effectively applying the reduction identity to the carry. + v.l0 += 19 * c + + v.l1 += v.l0 >> 51 + v.l0 = v.l0 & maskLow51Bits + v.l2 += v.l1 >> 51 + v.l1 = v.l1 & maskLow51Bits + v.l3 += v.l2 >> 51 + v.l2 = v.l2 & maskLow51Bits + v.l4 += v.l3 >> 51 + v.l3 = v.l3 & maskLow51Bits + // no additional carry + v.l4 = v.l4 & maskLow51Bits + + return v +} + +// Add sets v = a + b, and returns v. +func (v *Element) Add(a, b *Element) *Element { + v.l0 = a.l0 + b.l0 + v.l1 = a.l1 + b.l1 + v.l2 = a.l2 + b.l2 + v.l3 = a.l3 + b.l3 + v.l4 = a.l4 + b.l4 + // Using the generic implementation here is actually faster than the + // assembly. Probably because the body of this function is so simple that + // the compiler can figure out better optimizations by inlining the carry + // propagation. TODO + return v.carryPropagateGeneric() +} + +// Subtract sets v = a - b, and returns v. +func (v *Element) Subtract(a, b *Element) *Element { + // We first add 2 * p, to guarantee the subtraction won't underflow, and + // then subtract b (which can be up to 2^255 + 2^13 * 19). + v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0 + v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1 + v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2 + v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3 + v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4 + return v.carryPropagate() +} + +// Negate sets v = -a, and returns v. +func (v *Element) Negate(a *Element) *Element { + return v.Subtract(feZero, a) +} + +// Invert sets v = 1/z mod p, and returns v. +// +// If z == 0, Invert returns v = 0. +func (v *Element) Invert(z *Element) *Element { + // Inversion is implemented as exponentiation with exponent p − 2. It uses the + // same sequence of 255 squarings and 11 multiplications as [Curve25519]. + var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element + + z2.Square(z) // 2 + t.Square(&z2) // 4 + t.Square(&t) // 8 + z9.Multiply(&t, z) // 9 + z11.Multiply(&z9, &z2) // 11 + t.Square(&z11) // 22 + z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0 + + t.Square(&z2_5_0) // 2^6 - 2^1 + for i := 0; i < 4; i++ { + t.Square(&t) // 2^10 - 2^5 + } + z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0 + + t.Square(&z2_10_0) // 2^11 - 2^1 + for i := 0; i < 9; i++ { + t.Square(&t) // 2^20 - 2^10 + } + z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0 + + t.Square(&z2_20_0) // 2^21 - 2^1 + for i := 0; i < 19; i++ { + t.Square(&t) // 2^40 - 2^20 + } + t.Multiply(&t, &z2_20_0) // 2^40 - 2^0 + + t.Square(&t) // 2^41 - 2^1 + for i := 0; i < 9; i++ { + t.Square(&t) // 2^50 - 2^10 + } + z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0 + + t.Square(&z2_50_0) // 2^51 - 2^1 + for i := 0; i < 49; i++ { + t.Square(&t) // 2^100 - 2^50 + } + z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0 + + t.Square(&z2_100_0) // 2^101 - 2^1 + for i := 0; i < 99; i++ { + t.Square(&t) // 2^200 - 2^100 + } + t.Multiply(&t, &z2_100_0) // 2^200 - 2^0 + + t.Square(&t) // 2^201 - 2^1 + for i := 0; i < 49; i++ { + t.Square(&t) // 2^250 - 2^50 + } + t.Multiply(&t, &z2_50_0) // 2^250 - 2^0 + + t.Square(&t) // 2^251 - 2^1 + t.Square(&t) // 2^252 - 2^2 + t.Square(&t) // 2^253 - 2^3 + t.Square(&t) // 2^254 - 2^4 + t.Square(&t) // 2^255 - 2^5 + + return v.Multiply(&t, &z11) // 2^255 - 21 +} + +// Set sets v = a, and returns v. +func (v *Element) Set(a *Element) *Element { + *v = *a + return v +} + +// SetBytes sets v to x, which must be a 32-byte little-endian encoding. +// +// Consistent with RFC 7748, the most significant bit (the high bit of the +// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1) +// are accepted. Note that this is laxer than specified by RFC 8032. +func (v *Element) SetBytes(x []byte) *Element { + if len(x) != 32 { + panic("edwards25519: invalid field element input size") + } + + // Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51). + v.l0 = binary.LittleEndian.Uint64(x[0:8]) + v.l0 &= maskLow51Bits + // Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51). + v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3 + v.l1 &= maskLow51Bits + // Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51). + v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6 + v.l2 &= maskLow51Bits + // Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51). + v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1 + v.l3 &= maskLow51Bits + // Bits 204:251 (bytes 24:32, bits 192:256, shift 12, mask 51). + // Note: not bytes 25:33, shift 4, to avoid overread. + v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12 + v.l4 &= maskLow51Bits + + return v +} + +// Bytes returns the canonical 32-byte little-endian encoding of v. +func (v *Element) Bytes() []byte { + // This function is outlined to make the allocations inline in the caller + // rather than happen on the heap. + var out [32]byte + return v.bytes(&out) +} + +func (v *Element) bytes(out *[32]byte) []byte { + t := *v + t.reduce() + + var buf [8]byte + for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} { + bitsOffset := i * 51 + binary.LittleEndian.PutUint64(buf[:], l<= len(out) { + break + } + out[off] |= bb + } + } + + return out[:] +} + +// Equal returns 1 if v and u are equal, and 0 otherwise. +func (v *Element) Equal(u *Element) int { + sa, sv := u.Bytes(), v.Bytes() + return subtle.ConstantTimeCompare(sa, sv) +} + +// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise. +func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) } + +// Select sets v to a if cond == 1, and to b if cond == 0. +func (v *Element) Select(a, b *Element, cond int) *Element { + m := mask64Bits(cond) + v.l0 = (m & a.l0) | (^m & b.l0) + v.l1 = (m & a.l1) | (^m & b.l1) + v.l2 = (m & a.l2) | (^m & b.l2) + v.l3 = (m & a.l3) | (^m & b.l3) + v.l4 = (m & a.l4) | (^m & b.l4) + return v +} + +// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v. +func (v *Element) Swap(u *Element, cond int) { + m := mask64Bits(cond) + t := m & (v.l0 ^ u.l0) + v.l0 ^= t + u.l0 ^= t + t = m & (v.l1 ^ u.l1) + v.l1 ^= t + u.l1 ^= t + t = m & (v.l2 ^ u.l2) + v.l2 ^= t + u.l2 ^= t + t = m & (v.l3 ^ u.l3) + v.l3 ^= t + u.l3 ^= t + t = m & (v.l4 ^ u.l4) + v.l4 ^= t + u.l4 ^= t +} + +// IsNegative returns 1 if v is negative, and 0 otherwise. +func (v *Element) IsNegative() int { + return int(v.Bytes()[0] & 1) +} + +// Absolute sets v to |u|, and returns v. +func (v *Element) Absolute(u *Element) *Element { + return v.Select(new(Element).Negate(u), u, u.IsNegative()) +} + +// Multiply sets v = x * y, and returns v. +func (v *Element) Multiply(x, y *Element) *Element { + feMul(v, x, y) + return v +} + +// Square sets v = x * x, and returns v. +func (v *Element) Square(x *Element) *Element { + feSquare(v, x) + return v +} + +// Mult32 sets v = x * y, and returns v. +func (v *Element) Mult32(x *Element, y uint32) *Element { + x0lo, x0hi := mul51(x.l0, y) + x1lo, x1hi := mul51(x.l1, y) + x2lo, x2hi := mul51(x.l2, y) + x3lo, x3hi := mul51(x.l3, y) + x4lo, x4hi := mul51(x.l4, y) + v.l0 = x0lo + 19*x4hi // carried over per the reduction identity + v.l1 = x1lo + x0hi + v.l2 = x2lo + x1hi + v.l3 = x3lo + x2hi + v.l4 = x4lo + x3hi + // The hi portions are going to be only 32 bits, plus any previous excess, + // so we can skip the carry propagation. + return v +} + +// mul51 returns lo + hi * 2⁵¹ = a * b. +func mul51(a uint64, b uint32) (lo uint64, hi uint64) { + mh, ml := bits.Mul64(a, uint64(b)) + lo = ml & maskLow51Bits + hi = (mh << 13) | (ml >> 51) + return +} + +// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3. +func (v *Element) Pow22523(x *Element) *Element { + var t0, t1, t2 Element + + t0.Square(x) // x^2 + t1.Square(&t0) // x^4 + t1.Square(&t1) // x^8 + t1.Multiply(x, &t1) // x^9 + t0.Multiply(&t0, &t1) // x^11 + t0.Square(&t0) // x^22 + t0.Multiply(&t1, &t0) // x^31 + t1.Square(&t0) // x^62 + for i := 1; i < 5; i++ { // x^992 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1 + t1.Square(&t0) // 2^11 - 2 + for i := 1; i < 10; i++ { // 2^20 - 2^10 + t1.Square(&t1) + } + t1.Multiply(&t1, &t0) // 2^20 - 1 + t2.Square(&t1) // 2^21 - 2 + for i := 1; i < 20; i++ { // 2^40 - 2^20 + t2.Square(&t2) + } + t1.Multiply(&t2, &t1) // 2^40 - 1 + t1.Square(&t1) // 2^41 - 2 + for i := 1; i < 10; i++ { // 2^50 - 2^10 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // 2^50 - 1 + t1.Square(&t0) // 2^51 - 2 + for i := 1; i < 50; i++ { // 2^100 - 2^50 + t1.Square(&t1) + } + t1.Multiply(&t1, &t0) // 2^100 - 1 + t2.Square(&t1) // 2^101 - 2 + for i := 1; i < 100; i++ { // 2^200 - 2^100 + t2.Square(&t2) + } + t1.Multiply(&t2, &t1) // 2^200 - 1 + t1.Square(&t1) // 2^201 - 2 + for i := 1; i < 50; i++ { // 2^250 - 2^50 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // 2^250 - 1 + t0.Square(&t0) // 2^251 - 2 + t0.Square(&t0) // 2^252 - 4 + return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3) +} + +// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion. +var sqrtM1 = &Element{1718705420411056, 234908883556509, + 2233514472574048, 2117202627021982, 765476049583133} + +// SqrtRatio sets r to the non-negative square root of the ratio of u and v. +// +// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio +// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00, +// and returns r and 0. +func (r *Element) SqrtRatio(u, v *Element) (rr *Element, wasSquare int) { + var a, b Element + + // r = (u * v3) * (u * v7)^((p-5)/8) + v2 := a.Square(v) + uv3 := b.Multiply(u, b.Multiply(v2, v)) + uv7 := a.Multiply(uv3, a.Square(v2)) + r.Multiply(uv3, r.Pow22523(uv7)) + + check := a.Multiply(v, a.Square(r)) // check = v * r^2 + + uNeg := b.Negate(u) + correctSignSqrt := check.Equal(u) + flippedSignSqrt := check.Equal(uNeg) + flippedSignSqrtI := check.Equal(uNeg.Multiply(uNeg, sqrtM1)) + + rPrime := b.Multiply(r, sqrtM1) // r_prime = SQRT_M1 * r + // r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r) + r.Select(rPrime, r, flippedSignSqrt|flippedSignSqrtI) + + r.Absolute(r) // Choose the nonnegative square root. + return r, correctSignSqrt | flippedSignSqrt +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_alias_test.go b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go new file mode 100644 index 000000000..64e57c4f3 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go @@ -0,0 +1,126 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import ( + "testing" + "testing/quick" +) + +func checkAliasingOneArg(f func(v, x *Element) *Element) func(v, x Element) bool { + return func(v, x Element) bool { + x1, v1 := x, x + + // Calculate a reference f(x) without aliasing. + if out := f(&v, &x); out != &v && isInBounds(out) { + return false + } + + // Test aliasing the argument and the receiver. + if out := f(&v1, &v1); out != &v1 || v1 != v { + return false + } + + // Ensure the arguments was not modified. + return x == x1 + } +} + +func checkAliasingTwoArgs(f func(v, x, y *Element) *Element) func(v, x, y Element) bool { + return func(v, x, y Element) bool { + x1, y1, v1 := x, y, Element{} + + // Calculate a reference f(x, y) without aliasing. + if out := f(&v, &x, &y); out != &v && isInBounds(out) { + return false + } + + // Test aliasing the first argument and the receiver. + v1 = x + if out := f(&v1, &v1, &y); out != &v1 || v1 != v { + return false + } + // Test aliasing the second argument and the receiver. + v1 = y + if out := f(&v1, &x, &v1); out != &v1 || v1 != v { + return false + } + + // Calculate a reference f(x, x) without aliasing. + if out := f(&v, &x, &x); out != &v { + return false + } + + // Test aliasing the first argument and the receiver. + v1 = x + if out := f(&v1, &v1, &x); out != &v1 || v1 != v { + return false + } + // Test aliasing the second argument and the receiver. + v1 = x + if out := f(&v1, &x, &v1); out != &v1 || v1 != v { + return false + } + // Test aliasing both arguments and the receiver. + v1 = x + if out := f(&v1, &v1, &v1); out != &v1 || v1 != v { + return false + } + + // Ensure the arguments were not modified. + return x == x1 && y == y1 + } +} + +// TestAliasing checks that receivers and arguments can alias each other without +// leading to incorrect results. That is, it ensures that it's safe to write +// +// v.Invert(v) +// +// or +// +// v.Add(v, v) +// +// without any of the inputs getting clobbered by the output being written. +func TestAliasing(t *testing.T) { + type target struct { + name string + oneArgF func(v, x *Element) *Element + twoArgsF func(v, x, y *Element) *Element + } + for _, tt := range []target{ + {name: "Absolute", oneArgF: (*Element).Absolute}, + {name: "Invert", oneArgF: (*Element).Invert}, + {name: "Negate", oneArgF: (*Element).Negate}, + {name: "Set", oneArgF: (*Element).Set}, + {name: "Square", oneArgF: (*Element).Square}, + {name: "Multiply", twoArgsF: (*Element).Multiply}, + {name: "Add", twoArgsF: (*Element).Add}, + {name: "Subtract", twoArgsF: (*Element).Subtract}, + { + name: "Select0", + twoArgsF: func(v, x, y *Element) *Element { + return (*Element).Select(v, x, y, 0) + }, + }, + { + name: "Select1", + twoArgsF: func(v, x, y *Element) *Element { + return (*Element).Select(v, x, y, 1) + }, + }, + } { + var err error + switch { + case tt.oneArgF != nil: + err = quick.Check(checkAliasingOneArg(tt.oneArgF), &quick.Config{MaxCountScale: 1 << 8}) + case tt.twoArgsF != nil: + err = quick.Check(checkAliasingTwoArgs(tt.twoArgsF), &quick.Config{MaxCountScale: 1 << 8}) + } + if err != nil { + t.Errorf("%v: %v", tt.name, err) + } + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.go b/openpgp/internal/ecc/curve25519/field/fe_amd64.go new file mode 100644 index 000000000..edcf163c4 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.go @@ -0,0 +1,16 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +//go:build amd64 && gc && !purego +// +build amd64,gc,!purego + +package field + +// feMul sets out = a * b. It works like feMulGeneric. +// +//go:noescape +func feMul(out *Element, a *Element, b *Element) + +// feSquare sets out = a * a. It works like feSquareGeneric. +// +//go:noescape +func feSquare(out *Element, a *Element) diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.s b/openpgp/internal/ecc/curve25519/field/fe_amd64.s new file mode 100644 index 000000000..293f013c9 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.s @@ -0,0 +1,379 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +//go:build amd64 && gc && !purego +// +build amd64,gc,!purego + +#include "textflag.h" + +// func feMul(out *Element, a *Element, b *Element) +TEXT ·feMul(SB), NOSPLIT, $0-24 + MOVQ a+8(FP), CX + MOVQ b+16(FP), BX + + // r0 = a0×b0 + MOVQ (CX), AX + MULQ (BX) + MOVQ AX, DI + MOVQ DX, SI + + // r0 += 19×a1×b4 + MOVQ 8(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a2×b3 + MOVQ 16(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a3×b2 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 16(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a4×b1 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 8(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r1 = a0×b1 + MOVQ (CX), AX + MULQ 8(BX) + MOVQ AX, R9 + MOVQ DX, R8 + + // r1 += a1×b0 + MOVQ 8(CX), AX + MULQ (BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a2×b4 + MOVQ 16(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a3×b3 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a4×b2 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 16(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r2 = a0×b2 + MOVQ (CX), AX + MULQ 16(BX) + MOVQ AX, R11 + MOVQ DX, R10 + + // r2 += a1×b1 + MOVQ 8(CX), AX + MULQ 8(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += a2×b0 + MOVQ 16(CX), AX + MULQ (BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += 19×a3×b4 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += 19×a4×b3 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r3 = a0×b3 + MOVQ (CX), AX + MULQ 24(BX) + MOVQ AX, R13 + MOVQ DX, R12 + + // r3 += a1×b2 + MOVQ 8(CX), AX + MULQ 16(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += a2×b1 + MOVQ 16(CX), AX + MULQ 8(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += a3×b0 + MOVQ 24(CX), AX + MULQ (BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += 19×a4×b4 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r4 = a0×b4 + MOVQ (CX), AX + MULQ 32(BX) + MOVQ AX, R15 + MOVQ DX, R14 + + // r4 += a1×b3 + MOVQ 8(CX), AX + MULQ 24(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a2×b2 + MOVQ 16(CX), AX + MULQ 16(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a3×b1 + MOVQ 24(CX), AX + MULQ 8(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a4×b0 + MOVQ 32(CX), AX + MULQ (BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // First reduction chain + MOVQ $0x0007ffffffffffff, AX + SHLQ $0x0d, DI, SI + SHLQ $0x0d, R9, R8 + SHLQ $0x0d, R11, R10 + SHLQ $0x0d, R13, R12 + SHLQ $0x0d, R15, R14 + ANDQ AX, DI + IMUL3Q $0x13, R14, R14 + ADDQ R14, DI + ANDQ AX, R9 + ADDQ SI, R9 + ANDQ AX, R11 + ADDQ R8, R11 + ANDQ AX, R13 + ADDQ R10, R13 + ANDQ AX, R15 + ADDQ R12, R15 + + // Second reduction chain (carryPropagate) + MOVQ DI, SI + SHRQ $0x33, SI + MOVQ R9, R8 + SHRQ $0x33, R8 + MOVQ R11, R10 + SHRQ $0x33, R10 + MOVQ R13, R12 + SHRQ $0x33, R12 + MOVQ R15, R14 + SHRQ $0x33, R14 + ANDQ AX, DI + IMUL3Q $0x13, R14, R14 + ADDQ R14, DI + ANDQ AX, R9 + ADDQ SI, R9 + ANDQ AX, R11 + ADDQ R8, R11 + ANDQ AX, R13 + ADDQ R10, R13 + ANDQ AX, R15 + ADDQ R12, R15 + + // Store output + MOVQ out+0(FP), AX + MOVQ DI, (AX) + MOVQ R9, 8(AX) + MOVQ R11, 16(AX) + MOVQ R13, 24(AX) + MOVQ R15, 32(AX) + RET + +// func feSquare(out *Element, a *Element) +TEXT ·feSquare(SB), NOSPLIT, $0-16 + MOVQ a+8(FP), CX + + // r0 = l0×l0 + MOVQ (CX), AX + MULQ (CX) + MOVQ AX, SI + MOVQ DX, BX + + // r0 += 38×l1×l4 + MOVQ 8(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, SI + ADCQ DX, BX + + // r0 += 38×l2×l3 + MOVQ 16(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 24(CX) + ADDQ AX, SI + ADCQ DX, BX + + // r1 = 2×l0×l1 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 8(CX) + MOVQ AX, R8 + MOVQ DX, DI + + // r1 += 38×l2×l4 + MOVQ 16(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, R8 + ADCQ DX, DI + + // r1 += 19×l3×l3 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(CX) + ADDQ AX, R8 + ADCQ DX, DI + + // r2 = 2×l0×l2 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 16(CX) + MOVQ AX, R10 + MOVQ DX, R9 + + // r2 += l1×l1 + MOVQ 8(CX), AX + MULQ 8(CX) + ADDQ AX, R10 + ADCQ DX, R9 + + // r2 += 38×l3×l4 + MOVQ 24(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, R10 + ADCQ DX, R9 + + // r3 = 2×l0×l3 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 24(CX) + MOVQ AX, R12 + MOVQ DX, R11 + + // r3 += 2×l1×l2 + MOVQ 8(CX), AX + IMUL3Q $0x02, AX, AX + MULQ 16(CX) + ADDQ AX, R12 + ADCQ DX, R11 + + // r3 += 19×l4×l4 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(CX) + ADDQ AX, R12 + ADCQ DX, R11 + + // r4 = 2×l0×l4 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 32(CX) + MOVQ AX, R14 + MOVQ DX, R13 + + // r4 += 2×l1×l3 + MOVQ 8(CX), AX + IMUL3Q $0x02, AX, AX + MULQ 24(CX) + ADDQ AX, R14 + ADCQ DX, R13 + + // r4 += l2×l2 + MOVQ 16(CX), AX + MULQ 16(CX) + ADDQ AX, R14 + ADCQ DX, R13 + + // First reduction chain + MOVQ $0x0007ffffffffffff, AX + SHLQ $0x0d, SI, BX + SHLQ $0x0d, R8, DI + SHLQ $0x0d, R10, R9 + SHLQ $0x0d, R12, R11 + SHLQ $0x0d, R14, R13 + ANDQ AX, SI + IMUL3Q $0x13, R13, R13 + ADDQ R13, SI + ANDQ AX, R8 + ADDQ BX, R8 + ANDQ AX, R10 + ADDQ DI, R10 + ANDQ AX, R12 + ADDQ R9, R12 + ANDQ AX, R14 + ADDQ R11, R14 + + // Second reduction chain (carryPropagate) + MOVQ SI, BX + SHRQ $0x33, BX + MOVQ R8, DI + SHRQ $0x33, DI + MOVQ R10, R9 + SHRQ $0x33, R9 + MOVQ R12, R11 + SHRQ $0x33, R11 + MOVQ R14, R13 + SHRQ $0x33, R13 + ANDQ AX, SI + IMUL3Q $0x13, R13, R13 + ADDQ R13, SI + ANDQ AX, R8 + ADDQ BX, R8 + ANDQ AX, R10 + ADDQ DI, R10 + ANDQ AX, R12 + ADDQ R9, R12 + ANDQ AX, R14 + ADDQ R11, R14 + + // Store output + MOVQ out+0(FP), AX + MOVQ SI, (AX) + MOVQ R8, 8(AX) + MOVQ R10, 16(AX) + MOVQ R12, 24(AX) + MOVQ R14, 32(AX) + RET diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go b/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go new file mode 100644 index 000000000..ddb6c9b8f --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go @@ -0,0 +1,12 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !amd64 || !gc || purego +// +build !amd64 !gc purego + +package field + +func feMul(v, x, y *Element) { feMulGeneric(v, x, y) } + +func feSquare(v, x *Element) { feSquareGeneric(v, x) } diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64.go b/openpgp/internal/ecc/curve25519/field/fe_arm64.go new file mode 100644 index 000000000..af459ef51 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64.go @@ -0,0 +1,16 @@ +// Copyright (c) 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build arm64 && gc && !purego +// +build arm64,gc,!purego + +package field + +//go:noescape +func carryPropagate(v *Element) + +func (v *Element) carryPropagate() *Element { + carryPropagate(v) + return v +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64.s b/openpgp/internal/ecc/curve25519/field/fe_arm64.s new file mode 100644 index 000000000..5c91e4589 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64.s @@ -0,0 +1,43 @@ +// Copyright (c) 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build arm64 && gc && !purego +// +build arm64,gc,!purego + +#include "textflag.h" + +// carryPropagate works exactly like carryPropagateGeneric and uses the +// same AND, ADD, and LSR+MADD instructions emitted by the compiler, but +// avoids loading R0-R4 twice and uses LDP and STP. +// +// See https://golang.org/issues/43145 for the main compiler issue. +// +// func carryPropagate(v *Element) +TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8 + MOVD v+0(FP), R20 + + LDP 0(R20), (R0, R1) + LDP 16(R20), (R2, R3) + MOVD 32(R20), R4 + + AND $0x7ffffffffffff, R0, R10 + AND $0x7ffffffffffff, R1, R11 + AND $0x7ffffffffffff, R2, R12 + AND $0x7ffffffffffff, R3, R13 + AND $0x7ffffffffffff, R4, R14 + + ADD R0>>51, R11, R11 + ADD R1>>51, R12, R12 + ADD R2>>51, R13, R13 + ADD R3>>51, R14, R14 + // R4>>51 * 19 + R10 -> R10 + LSR $51, R4, R21 + MOVD $19, R22 + MADD R22, R10, R21, R10 + + STP (R10, R11), 0(R20) + STP (R12, R13), 16(R20) + MOVD R14, 32(R20) + + RET diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go b/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go new file mode 100644 index 000000000..234a5b2e5 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go @@ -0,0 +1,12 @@ +// Copyright (c) 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !arm64 || !gc || purego +// +build !arm64 !gc purego + +package field + +func (v *Element) carryPropagate() *Element { + return v.carryPropagateGeneric() +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_bench_test.go b/openpgp/internal/ecc/curve25519/field/fe_bench_test.go new file mode 100644 index 000000000..77dc06cf9 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_bench_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import "testing" + +func BenchmarkAdd(b *testing.B) { + var x, y Element + x.One() + y.Add(feOne, feOne) + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Add(&x, &y) + } +} + +func BenchmarkMultiply(b *testing.B) { + var x, y Element + x.One() + y.Add(feOne, feOne) + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Multiply(&x, &y) + } +} + +func BenchmarkMult32(b *testing.B) { + var x Element + x.One() + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Mult32(&x, 0xaa42aa42) + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_generic.go b/openpgp/internal/ecc/curve25519/field/fe_generic.go new file mode 100644 index 000000000..7b5b78cbd --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_generic.go @@ -0,0 +1,264 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import "math/bits" + +// uint128 holds a 128-bit number as two 64-bit limbs, for use with the +// bits.Mul64 and bits.Add64 intrinsics. +type uint128 struct { + lo, hi uint64 +} + +// mul64 returns a * b. +func mul64(a, b uint64) uint128 { + hi, lo := bits.Mul64(a, b) + return uint128{lo, hi} +} + +// addMul64 returns v + a * b. +func addMul64(v uint128, a, b uint64) uint128 { + hi, lo := bits.Mul64(a, b) + lo, c := bits.Add64(lo, v.lo, 0) + hi, _ = bits.Add64(hi, v.hi, c) + return uint128{lo, hi} +} + +// shiftRightBy51 returns a >> 51. a is assumed to be at most 115 bits. +func shiftRightBy51(a uint128) uint64 { + return (a.hi << (64 - 51)) | (a.lo >> 51) +} + +func feMulGeneric(v, a, b *Element) { + a0 := a.l0 + a1 := a.l1 + a2 := a.l2 + a3 := a.l3 + a4 := a.l4 + + b0 := b.l0 + b1 := b.l1 + b2 := b.l2 + b3 := b.l3 + b4 := b.l4 + + // Limb multiplication works like pen-and-paper columnar multiplication, but + // with 51-bit limbs instead of digits. + // + // a4 a3 a2 a1 a0 x + // b4 b3 b2 b1 b0 = + // ------------------------ + // a4b0 a3b0 a2b0 a1b0 a0b0 + + // a4b1 a3b1 a2b1 a1b1 a0b1 + + // a4b2 a3b2 a2b2 a1b2 a0b2 + + // a4b3 a3b3 a2b3 a1b3 a0b3 + + // a4b4 a3b4 a2b4 a1b4 a0b4 = + // ---------------------------------------------- + // r8 r7 r6 r5 r4 r3 r2 r1 r0 + // + // We can then use the reduction identity (a * 2²⁵⁵ + b = a * 19 + b) to + // reduce the limbs that would overflow 255 bits. r5 * 2²⁵⁵ becomes 19 * r5, + // r6 * 2³⁰⁶ becomes 19 * r6 * 2⁵¹, etc. + // + // Reduction can be carried out simultaneously to multiplication. For + // example, we do not compute r5: whenever the result of a multiplication + // belongs to r5, like a1b4, we multiply it by 19 and add the result to r0. + // + // a4b0 a3b0 a2b0 a1b0 a0b0 + + // a3b1 a2b1 a1b1 a0b1 19×a4b1 + + // a2b2 a1b2 a0b2 19×a4b2 19×a3b2 + + // a1b3 a0b3 19×a4b3 19×a3b3 19×a2b3 + + // a0b4 19×a4b4 19×a3b4 19×a2b4 19×a1b4 = + // -------------------------------------- + // r4 r3 r2 r1 r0 + // + // Finally we add up the columns into wide, overlapping limbs. + + a1_19 := a1 * 19 + a2_19 := a2 * 19 + a3_19 := a3 * 19 + a4_19 := a4 * 19 + + // r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1) + r0 := mul64(a0, b0) + r0 = addMul64(r0, a1_19, b4) + r0 = addMul64(r0, a2_19, b3) + r0 = addMul64(r0, a3_19, b2) + r0 = addMul64(r0, a4_19, b1) + + // r1 = a0×b1 + a1×b0 + 19×(a2×b4 + a3×b3 + a4×b2) + r1 := mul64(a0, b1) + r1 = addMul64(r1, a1, b0) + r1 = addMul64(r1, a2_19, b4) + r1 = addMul64(r1, a3_19, b3) + r1 = addMul64(r1, a4_19, b2) + + // r2 = a0×b2 + a1×b1 + a2×b0 + 19×(a3×b4 + a4×b3) + r2 := mul64(a0, b2) + r2 = addMul64(r2, a1, b1) + r2 = addMul64(r2, a2, b0) + r2 = addMul64(r2, a3_19, b4) + r2 = addMul64(r2, a4_19, b3) + + // r3 = a0×b3 + a1×b2 + a2×b1 + a3×b0 + 19×a4×b4 + r3 := mul64(a0, b3) + r3 = addMul64(r3, a1, b2) + r3 = addMul64(r3, a2, b1) + r3 = addMul64(r3, a3, b0) + r3 = addMul64(r3, a4_19, b4) + + // r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0 + r4 := mul64(a0, b4) + r4 = addMul64(r4, a1, b3) + r4 = addMul64(r4, a2, b2) + r4 = addMul64(r4, a3, b1) + r4 = addMul64(r4, a4, b0) + + // After the multiplication, we need to reduce (carry) the five coefficients + // to obtain a result with limbs that are at most slightly larger than 2⁵¹, + // to respect the Element invariant. + // + // Overall, the reduction works the same as carryPropagate, except with + // wider inputs: we take the carry for each coefficient by shifting it right + // by 51, and add it to the limb above it. The top carry is multiplied by 19 + // according to the reduction identity and added to the lowest limb. + // + // The largest coefficient (r0) will be at most 111 bits, which guarantees + // that all carries are at most 111 - 51 = 60 bits, which fits in a uint64. + // + // r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1) + // r0 < 2⁵²×2⁵² + 19×(2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵²) + // r0 < (1 + 19 × 4) × 2⁵² × 2⁵² + // r0 < 2⁷ × 2⁵² × 2⁵² + // r0 < 2¹¹¹ + // + // Moreover, the top coefficient (r4) is at most 107 bits, so c4 is at most + // 56 bits, and c4 * 19 is at most 61 bits, which again fits in a uint64 and + // allows us to easily apply the reduction identity. + // + // r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0 + // r4 < 5 × 2⁵² × 2⁵² + // r4 < 2¹⁰⁷ + // + + c0 := shiftRightBy51(r0) + c1 := shiftRightBy51(r1) + c2 := shiftRightBy51(r2) + c3 := shiftRightBy51(r3) + c4 := shiftRightBy51(r4) + + rr0 := r0.lo&maskLow51Bits + c4*19 + rr1 := r1.lo&maskLow51Bits + c0 + rr2 := r2.lo&maskLow51Bits + c1 + rr3 := r3.lo&maskLow51Bits + c2 + rr4 := r4.lo&maskLow51Bits + c3 + + // Now all coefficients fit into 64-bit registers but are still too large to + // be passed around as a Element. We therefore do one last carry chain, + // where the carries will be small enough to fit in the wiggle room above 2⁵¹. + *v = Element{rr0, rr1, rr2, rr3, rr4} + v.carryPropagate() +} + +func feSquareGeneric(v, a *Element) { + l0 := a.l0 + l1 := a.l1 + l2 := a.l2 + l3 := a.l3 + l4 := a.l4 + + // Squaring works precisely like multiplication above, but thanks to its + // symmetry we get to group a few terms together. + // + // l4 l3 l2 l1 l0 x + // l4 l3 l2 l1 l0 = + // ------------------------ + // l4l0 l3l0 l2l0 l1l0 l0l0 + + // l4l1 l3l1 l2l1 l1l1 l0l1 + + // l4l2 l3l2 l2l2 l1l2 l0l2 + + // l4l3 l3l3 l2l3 l1l3 l0l3 + + // l4l4 l3l4 l2l4 l1l4 l0l4 = + // ---------------------------------------------- + // r8 r7 r6 r5 r4 r3 r2 r1 r0 + // + // l4l0 l3l0 l2l0 l1l0 l0l0 + + // l3l1 l2l1 l1l1 l0l1 19×l4l1 + + // l2l2 l1l2 l0l2 19×l4l2 19×l3l2 + + // l1l3 l0l3 19×l4l3 19×l3l3 19×l2l3 + + // l0l4 19×l4l4 19×l3l4 19×l2l4 19×l1l4 = + // -------------------------------------- + // r4 r3 r2 r1 r0 + // + // With precomputed 2×, 19×, and 2×19× terms, we can compute each limb with + // only three Mul64 and four Add64, instead of five and eight. + + l0_2 := l0 * 2 + l1_2 := l1 * 2 + + l1_38 := l1 * 38 + l2_38 := l2 * 38 + l3_38 := l3 * 38 + + l3_19 := l3 * 19 + l4_19 := l4 * 19 + + // r0 = l0×l0 + 19×(l1×l4 + l2×l3 + l3×l2 + l4×l1) = l0×l0 + 19×2×(l1×l4 + l2×l3) + r0 := mul64(l0, l0) + r0 = addMul64(r0, l1_38, l4) + r0 = addMul64(r0, l2_38, l3) + + // r1 = l0×l1 + l1×l0 + 19×(l2×l4 + l3×l3 + l4×l2) = 2×l0×l1 + 19×2×l2×l4 + 19×l3×l3 + r1 := mul64(l0_2, l1) + r1 = addMul64(r1, l2_38, l4) + r1 = addMul64(r1, l3_19, l3) + + // r2 = l0×l2 + l1×l1 + l2×l0 + 19×(l3×l4 + l4×l3) = 2×l0×l2 + l1×l1 + 19×2×l3×l4 + r2 := mul64(l0_2, l2) + r2 = addMul64(r2, l1, l1) + r2 = addMul64(r2, l3_38, l4) + + // r3 = l0×l3 + l1×l2 + l2×l1 + l3×l0 + 19×l4×l4 = 2×l0×l3 + 2×l1×l2 + 19×l4×l4 + r3 := mul64(l0_2, l3) + r3 = addMul64(r3, l1_2, l2) + r3 = addMul64(r3, l4_19, l4) + + // r4 = l0×l4 + l1×l3 + l2×l2 + l3×l1 + l4×l0 = 2×l0×l4 + 2×l1×l3 + l2×l2 + r4 := mul64(l0_2, l4) + r4 = addMul64(r4, l1_2, l3) + r4 = addMul64(r4, l2, l2) + + c0 := shiftRightBy51(r0) + c1 := shiftRightBy51(r1) + c2 := shiftRightBy51(r2) + c3 := shiftRightBy51(r3) + c4 := shiftRightBy51(r4) + + rr0 := r0.lo&maskLow51Bits + c4*19 + rr1 := r1.lo&maskLow51Bits + c0 + rr2 := r2.lo&maskLow51Bits + c1 + rr3 := r3.lo&maskLow51Bits + c2 + rr4 := r4.lo&maskLow51Bits + c3 + + *v = Element{rr0, rr1, rr2, rr3, rr4} + v.carryPropagate() +} + +// carryPropagate brings the limbs below 52 bits by applying the reduction +// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry. TODO inline +func (v *Element) carryPropagateGeneric() *Element { + c0 := v.l0 >> 51 + c1 := v.l1 >> 51 + c2 := v.l2 >> 51 + c3 := v.l3 >> 51 + c4 := v.l4 >> 51 + + v.l0 = v.l0&maskLow51Bits + c4*19 + v.l1 = v.l1&maskLow51Bits + c0 + v.l2 = v.l2&maskLow51Bits + c1 + v.l3 = v.l3&maskLow51Bits + c2 + v.l4 = v.l4&maskLow51Bits + c3 + + return v +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_test.go b/openpgp/internal/ecc/curve25519/field/fe_test.go new file mode 100644 index 000000000..b484459ff --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_test.go @@ -0,0 +1,558 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "io" + "math/big" + "math/bits" + mathrand "math/rand" + "reflect" + "testing" + "testing/quick" +) + +func (v Element) String() string { + return hex.EncodeToString(v.Bytes()) +} + +// quickCheckConfig1024 will make each quickcheck test run (1024 * -quickchecks) +// times. The default value of -quickchecks is 100. +var quickCheckConfig1024 = &quick.Config{MaxCountScale: 1 << 10} + +func generateFieldElement(rand *mathrand.Rand) Element { + const maskLow52Bits = (1 << 52) - 1 + return Element{ + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + } +} + +// weirdLimbs can be combined to generate a range of edge-case field elements. +// 0 and -1 are intentionally more weighted, as they combine well. +var ( + weirdLimbs51 = []uint64{ + 0, 0, 0, 0, + 1, + 19 - 1, + 19, + 0x2aaaaaaaaaaaa, + 0x5555555555555, + (1 << 51) - 20, + (1 << 51) - 19, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + } + weirdLimbs52 = []uint64{ + 0, 0, 0, 0, 0, 0, + 1, + 19 - 1, + 19, + 0x2aaaaaaaaaaaa, + 0x5555555555555, + (1 << 51) - 20, + (1 << 51) - 19, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + 1 << 51, + (1 << 51) + 1, + (1 << 52) - 19, + (1 << 52) - 1, + } +) + +func generateWeirdFieldElement(rand *mathrand.Rand) Element { + return Element{ + weirdLimbs52[rand.Intn(len(weirdLimbs52))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + } +} + +func (Element) Generate(rand *mathrand.Rand, size int) reflect.Value { + if rand.Intn(2) == 0 { + return reflect.ValueOf(generateWeirdFieldElement(rand)) + } + return reflect.ValueOf(generateFieldElement(rand)) +} + +// isInBounds returns whether the element is within the expected bit size bounds +// after a light reduction. +func isInBounds(x *Element) bool { + return bits.Len64(x.l0) <= 52 && + bits.Len64(x.l1) <= 52 && + bits.Len64(x.l2) <= 52 && + bits.Len64(x.l3) <= 52 && + bits.Len64(x.l4) <= 52 +} + +func TestMultiplyDistributesOverAdd(t *testing.T) { + multiplyDistributesOverAdd := func(x, y, z Element) bool { + // Compute t1 = (x+y)*z + t1 := new(Element) + t1.Add(&x, &y) + t1.Multiply(t1, &z) + + // Compute t2 = x*z + y*z + t2 := new(Element) + t3 := new(Element) + t2.Multiply(&x, &z) + t3.Multiply(&y, &z) + t2.Add(t2, t3) + + return t1.Equal(t2) == 1 && isInBounds(t1) && isInBounds(t2) + } + + if err := quick.Check(multiplyDistributesOverAdd, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestMul64to128(t *testing.T) { + a := uint64(5) + b := uint64(5) + r := mul64(a, b) + if r.lo != 0x19 || r.hi != 0 { + t.Errorf("lo-range wide mult failed, got %d + %d*(2**64)", r.lo, r.hi) + } + + a = uint64(18014398509481983) // 2^54 - 1 + b = uint64(18014398509481983) // 2^54 - 1 + r = mul64(a, b) + if r.lo != 0xff80000000000001 || r.hi != 0xfffffffffff { + t.Errorf("hi-range wide mult failed, got %d + %d*(2**64)", r.lo, r.hi) + } + + a = uint64(1125899906842661) + b = uint64(2097155) + r = mul64(a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + if r.lo != 16888498990613035 || r.hi != 640 { + t.Errorf("wrong answer: %d + %d*(2**64)", r.lo, r.hi) + } +} + +func TestSetBytesRoundTrip(t *testing.T) { + f1 := func(in [32]byte, fe Element) bool { + fe.SetBytes(in[:]) + + // Mask the most significant bit as it's ignored by SetBytes. (Now + // instead of earlier so we check the masking in SetBytes is working.) + in[len(in)-1] &= (1 << 7) - 1 + + return bytes.Equal(in[:], fe.Bytes()) && isInBounds(&fe) + } + if err := quick.Check(f1, nil); err != nil { + t.Errorf("failed bytes->FE->bytes round-trip: %v", err) + } + + f2 := func(fe, r Element) bool { + r.SetBytes(fe.Bytes()) + + // Intentionally not using Equal not to go through Bytes again. + // Calling reduce because both Generate and SetBytes can produce + // non-canonical representations. + fe.reduce() + r.reduce() + return fe == r + } + if err := quick.Check(f2, nil); err != nil { + t.Errorf("failed FE->bytes->FE round-trip: %v", err) + } + + // Check some fixed vectors from dalek + type feRTTest struct { + fe Element + b []byte + } + var tests = []feRTTest{ + { + fe: Element{358744748052810, 1691584618240980, 977650209285361, 1429865912637724, 560044844278676}, + b: []byte{74, 209, 69, 197, 70, 70, 161, 222, 56, 226, 229, 19, 112, 60, 25, 92, 187, 74, 222, 56, 50, 153, 51, 233, 40, 74, 57, 6, 160, 185, 213, 31}, + }, + { + fe: Element{84926274344903, 473620666599931, 365590438845504, 1028470286882429, 2146499180330972}, + b: []byte{199, 23, 106, 112, 61, 77, 216, 79, 186, 60, 11, 118, 13, 16, 103, 15, 42, 32, 83, 250, 44, 57, 204, 198, 78, 199, 253, 119, 146, 172, 3, 122}, + }, + } + + for _, tt := range tests { + b := tt.fe.Bytes() + if !bytes.Equal(b, tt.b) || new(Element).SetBytes(tt.b).Equal(&tt.fe) != 1 { + t.Errorf("Failed fixed roundtrip: %v", tt) + } + } +} + +func swapEndianness(buf []byte) []byte { + for i := 0; i < len(buf)/2; i++ { + buf[i], buf[len(buf)-i-1] = buf[len(buf)-i-1], buf[i] + } + return buf +} + +func TestBytesBigEquivalence(t *testing.T) { + f1 := func(in [32]byte, fe, fe1 Element) bool { + fe.SetBytes(in[:]) + + in[len(in)-1] &= (1 << 7) - 1 // mask the most significant bit + b := new(big.Int).SetBytes(swapEndianness(in[:])) + fe1.fromBig(b) + + if fe != fe1 { + return false + } + + buf := make([]byte, 32) // pad with zeroes + copy(buf, swapEndianness(fe1.toBig().Bytes())) + + return bytes.Equal(fe.Bytes(), buf) && isInBounds(&fe) && isInBounds(&fe1) + } + if err := quick.Check(f1, nil); err != nil { + t.Error(err) + } +} + +// fromBig sets v = n, and returns v. The bit length of n must not exceed 256. +func (v *Element) fromBig(n *big.Int) *Element { + if n.BitLen() > 32*8 { + panic("edwards25519: invalid field element input size") + } + + buf := make([]byte, 0, 32) + for _, word := range n.Bits() { + for i := 0; i < bits.UintSize; i += 8 { + if len(buf) >= cap(buf) { + break + } + buf = append(buf, byte(word)) + word >>= 8 + } + } + + return v.SetBytes(buf[:32]) +} + +func (v *Element) fromDecimal(s string) *Element { + n, ok := new(big.Int).SetString(s, 10) + if !ok { + panic("not a valid decimal: " + s) + } + return v.fromBig(n) +} + +// toBig returns v as a big.Int. +func (v *Element) toBig() *big.Int { + buf := v.Bytes() + + words := make([]big.Word, 32*8/bits.UintSize) + for n := range words { + for i := 0; i < bits.UintSize; i += 8 { + if len(buf) == 0 { + break + } + words[n] |= big.Word(buf[0]) << big.Word(i) + buf = buf[1:] + } + } + + return new(big.Int).SetBits(words) +} + +func TestDecimalConstants(t *testing.T) { + sqrtM1String := "19681161376707505956807079304988542015446066515923890162744021073123829784752" + if exp := new(Element).fromDecimal(sqrtM1String); sqrtM1.Equal(exp) != 1 { + t.Errorf("sqrtM1 is %v, expected %v", sqrtM1, exp) + } + // d is in the parent package, and we don't want to expose d or fromDecimal. + // dString := "37095705934669439343138083508754565189542113879843219016388785533085940283555" + // if exp := new(Element).fromDecimal(dString); d.Equal(exp) != 1 { + // t.Errorf("d is %v, expected %v", d, exp) + // } +} + +func TestSetBytesRoundTripEdgeCases(t *testing.T) { + // TODO: values close to 0, close to 2^255-19, between 2^255-19 and 2^255-1, + // and between 2^255 and 2^256-1. Test both the documented SetBytes + // behavior, and that Bytes reduces them. +} + +// Tests self-consistency between Multiply and Square. +func TestConsistency(t *testing.T) { + var x Element + var x2, x2sq Element + + x = Element{1, 1, 1, 1, 1} + x2.Multiply(&x, &x) + x2sq.Square(&x) + + if x2 != x2sq { + t.Fatalf("all ones failed\nmul: %x\nsqr: %x\n", x2, x2sq) + } + + var bytes [32]byte + + _, err := io.ReadFull(rand.Reader, bytes[:]) + if err != nil { + t.Fatal(err) + } + x.SetBytes(bytes[:]) + + x2.Multiply(&x, &x) + x2sq.Square(&x) + + if x2 != x2sq { + t.Fatalf("all ones failed\nmul: %x\nsqr: %x\n", x2, x2sq) + } +} + +func TestEqual(t *testing.T) { + x := Element{1, 1, 1, 1, 1} + y := Element{5, 4, 3, 2, 1} + + eq := x.Equal(&x) + if eq != 1 { + t.Errorf("wrong about equality") + } + + eq = x.Equal(&y) + if eq != 0 { + t.Errorf("wrong about inequality") + } +} + +func TestInvert(t *testing.T) { + x := Element{1, 1, 1, 1, 1} + one := Element{1, 0, 0, 0, 0} + var xinv, r Element + + xinv.Invert(&x) + r.Multiply(&x, &xinv) + r.reduce() + + if one != r { + t.Errorf("inversion identity failed, got: %x", r) + } + + var bytes [32]byte + + _, err := io.ReadFull(rand.Reader, bytes[:]) + if err != nil { + t.Fatal(err) + } + x.SetBytes(bytes[:]) + + xinv.Invert(&x) + r.Multiply(&x, &xinv) + r.reduce() + + if one != r { + t.Errorf("random inversion identity failed, got: %x for field element %x", r, x) + } + + zero := Element{} + x.Set(&zero) + if xx := xinv.Invert(&x); xx != &xinv { + t.Errorf("inverting zero did not return the receiver") + } else if xinv.Equal(&zero) != 1 { + t.Errorf("inverting zero did not return zero") + } +} + +func TestSelectSwap(t *testing.T) { + a := Element{358744748052810, 1691584618240980, 977650209285361, 1429865912637724, 560044844278676} + b := Element{84926274344903, 473620666599931, 365590438845504, 1028470286882429, 2146499180330972} + + var c, d Element + + c.Select(&a, &b, 1) + d.Select(&a, &b, 0) + + if c.Equal(&a) != 1 || d.Equal(&b) != 1 { + t.Errorf("Select failed") + } + + c.Swap(&d, 0) + + if c.Equal(&a) != 1 || d.Equal(&b) != 1 { + t.Errorf("Swap failed") + } + + c.Swap(&d, 1) + + if c.Equal(&b) != 1 || d.Equal(&a) != 1 { + t.Errorf("Swap failed") + } +} + +func TestMult32(t *testing.T) { + mult32EquivalentToMul := func(x Element, y uint32) bool { + t1 := new(Element) + for i := 0; i < 100; i++ { + t1.Mult32(&x, y) + } + + ty := new(Element) + ty.l0 = uint64(y) + + t2 := new(Element) + for i := 0; i < 100; i++ { + t2.Multiply(&x, ty) + } + + return t1.Equal(t2) == 1 && isInBounds(t1) && isInBounds(t2) + } + + if err := quick.Check(mult32EquivalentToMul, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestSqrtRatio(t *testing.T) { + // From draft-irtf-cfrg-ristretto255-decaf448-00, Appendix A.4. + type test struct { + u, v string + wasSquare int + r string + } + var tests = []test{ + // If u is 0, the function is defined to return (0, TRUE), even if v + // is zero. Note that where used in this package, the denominator v + // is never zero. + { + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + 1, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // 0/1 == 0² + { + "0000000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 1, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // If u is non-zero and v is zero, defined to return (0, FALSE). + { + "0100000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + 0, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // 2/1 is not square in this field. + { + "0200000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 0, "3c5ff1b5d8e4113b871bd052f9e7bcd0582804c266ffb2d4f4203eb07fdb7c54", + }, + // 4/1 == 2² + { + "0400000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 1, "0200000000000000000000000000000000000000000000000000000000000000", + }, + // 1/4 == (2⁻¹)² == (2^(p-2))² per Euler's theorem + { + "0100000000000000000000000000000000000000000000000000000000000000", + "0400000000000000000000000000000000000000000000000000000000000000", + 1, "f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", + }, + } + + for i, tt := range tests { + u := new(Element).SetBytes(decodeHex(tt.u)) + v := new(Element).SetBytes(decodeHex(tt.v)) + want := new(Element).SetBytes(decodeHex(tt.r)) + got, wasSquare := new(Element).SqrtRatio(u, v) + if got.Equal(want) == 0 || wasSquare != tt.wasSquare { + t.Errorf("%d: got (%v, %v), want (%v, %v)", i, got, wasSquare, want, tt.wasSquare) + } + } +} + +func TestCarryPropagate(t *testing.T) { + asmLikeGeneric := func(a [5]uint64) bool { + t1 := &Element{a[0], a[1], a[2], a[3], a[4]} + t2 := &Element{a[0], a[1], a[2], a[3], a[4]} + + t1.carryPropagate() + t2.carryPropagateGeneric() + + if *t1 != *t2 { + t.Logf("got: %#v,\nexpected: %#v", t1, t2) + } + + return *t1 == *t2 && isInBounds(t2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } + + if !asmLikeGeneric([5]uint64{0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff}) { + t.Errorf("failed for {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff}") + } +} + +func TestFeSquare(t *testing.T) { + asmLikeGeneric := func(a Element) bool { + t1 := a + t2 := a + + feSquareGeneric(&t1, &t1) + feSquare(&t2, &t2) + + if t1 != t2 { + t.Logf("got: %#v,\nexpected: %#v", t1, t2) + } + + return t1 == t2 && isInBounds(&t2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestFeMul(t *testing.T) { + asmLikeGeneric := func(a, b Element) bool { + a1 := a + a2 := a + b1 := b + b2 := b + + feMulGeneric(&a1, &a1, &b1) + feMul(&a2, &a2, &b2) + + if a1 != a2 || b1 != b2 { + t.Logf("got: %#v,\nexpected: %#v", a1, a2) + t.Logf("got: %#v,\nexpected: %#v", b1, b2) + } + + return a1 == a2 && isInBounds(&a2) && + b1 == b2 && isInBounds(&b2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func decodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} diff --git a/openpgp/internal/ecc/curves.go b/openpgp/internal/ecc/curves.go index 5ed9c93b3..34c4ad860 100644 --- a/openpgp/internal/ecc/curves.go +++ b/openpgp/internal/ecc/curves.go @@ -16,6 +16,8 @@ type ECDSACurve interface { UnmarshalIntegerPoint([]byte) (x, y *big.Int) MarshalIntegerSecret(d *big.Int) []byte UnmarshalIntegerSecret(d []byte) *big.Int + MarshalFieldInteger(d *big.Int) []byte + UnmarshalFieldInteger(d []byte) *big.Int GenerateECDSA(rand io.Reader) (x, y, secret *big.Int, err error) Sign(rand io.Reader, x, y, d *big.Int, hash []byte) (r, s *big.Int, err error) Verify(x, y *big.Int, hash []byte, r, s *big.Int) bool diff --git a/openpgp/internal/ecc/generic.go b/openpgp/internal/ecc/generic.go index 1408e1120..2b997c6af 100644 --- a/openpgp/internal/ecc/generic.go +++ b/openpgp/internal/ecc/generic.go @@ -56,6 +56,15 @@ func (c *genericCurve) UnmarshalIntegerSecret(d []byte) *big.Int { return new(big.Int).SetBytes(d) } +func (c *genericCurve) MarshalFieldInteger(i *big.Int) (b []byte) { + b = make([]byte, (c.Curve.Params().BitSize+7)/8) + return i.FillBytes(b) +} + +func (c *genericCurve) UnmarshalFieldInteger(d []byte) *big.Int { + return new(big.Int).SetBytes(d) +} + func (c *genericCurve) GenerateECDH(rand io.Reader) (point, secret []byte, err error) { secret, x, y, err := elliptic.GenerateKey(c.Curve, rand) if err != nil { diff --git a/openpgp/internal/encoding/octetarray.go b/openpgp/internal/encoding/octetarray.go new file mode 100644 index 000000000..e5e4a8274 --- /dev/null +++ b/openpgp/internal/encoding/octetarray.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package encoding + +import ( + "io" +) + +// OctetArray is used to store a fixed-length field +type OctetArray struct { + length int + bytes []byte +} + +// NewOctetArray returns a OID initialized with bytes. +func NewOctetArray(bytes []byte) *OctetArray { + return &OctetArray{ + length: len(bytes), + bytes: bytes, + } +} + +func NewEmptyOctetArray(length int) *OctetArray { + return &OctetArray{ + length: length, + bytes: nil, + } +} + +// Bytes returns the decoded data. +func (o *OctetArray) Bytes() []byte { + return o.bytes +} + +// BitLength is the size in bits of the decoded data. +func (o *OctetArray) BitLength() uint16 { + return uint16(o.length * 8) +} + +// EncodedBytes returns the encoded data. +func (o *OctetArray) EncodedBytes() []byte { + if len(o.bytes) != o.length { + panic("invalid length") + } + return o.bytes +} + +// EncodedLength is the size in bytes of the encoded data. +func (o *OctetArray) EncodedLength() uint16 { + return uint16(o.length) +} + +// ReadFrom reads into b the next OID from r. +func (o *OctetArray) ReadFrom(r io.Reader) (int64, error) { + o.bytes = make([]byte, o.length) + + nn, err := io.ReadFull(r, o.bytes) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + return int64(nn), err +} diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go new file mode 100644 index 000000000..0c3b91233 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string.go @@ -0,0 +1,50 @@ +package encoding + +import ( + "io" +) + +type ShortByteString struct { + length uint8 + data []byte +} + +func NewShortByteString(data []byte) *ShortByteString { + byteLength := uint8(len(data)) + + return &ShortByteString{byteLength, data} +} + +func (byteString *ShortByteString) Bytes() []byte { + return byteString.data +} + +func (byteString *ShortByteString) BitLength() uint16 { + return uint16(byteString.length) * 8 +} + +func (byteString *ShortByteString) EncodedBytes() []byte { + encodedLength := [1]byte{ + uint8(byteString.length), + } + return append(encodedLength[:], byteString.data...) +} + +func (byteString *ShortByteString) EncodedLength() uint16 { + return uint16(byteString.length) + 1 +} + +func (byteString *ShortByteString) ReadFrom(r io.Reader) (int64, error) { + var lengthBytes [1]byte + if n, err := io.ReadFull(r, lengthBytes[:]); err != nil { + return int64(n), err + } + + byteString.length = uint8(lengthBytes[0]) + + byteString.data = make([]byte, byteString.length) + if n, err := io.ReadFull(r, byteString.data); err != nil { + return int64(n + 1), err + } + return int64(byteString.length + 1), nil +} diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go new file mode 100644 index 000000000..37510a355 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var octetStreamTests = []struct { + data []byte +}{ + { + data: []byte{0x0, 0x0, 0x0}, + }, + { + data: []byte{0x1, 0x2, 0x03}, + }, + { + data: make([]byte, 255), + }, +} + +func TestShortByteString(t *testing.T) { + for i, test := range octetStreamTests { + octetStream := NewShortByteString(test.data) + + if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { + t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) + } + + expectedBitLength := uint16(len(test.data)) * 8 + if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { + t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) + } + + expectedEncodedLength := uint16(len(test.data)) + 1 + if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { + t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) + } + + encodedBytes := octetStream.EncodedBytes() + if !bytes.Equal(encodedBytes[1:], test.data) { + t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) + } + + encodedLength := int(encodedBytes[0]) + if encodedLength != len(test.data) { + t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) + } + + newStream := new(ShortByteString) + newStream.ReadFrom(bytes.NewReader(encodedBytes)) + + if !checkEquality(newStream, octetStream) { + t.Errorf("#%d: bad parsing of encoded octet stream", i) + } + } +} + +func checkEquality(left *ShortByteString, right *ShortByteString) bool { + return (left.length == right.length) && (bytes.Equal(left.data, right.data)) +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 77213f66b..3dfaadd19 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -21,7 +21,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "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/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -319,6 +322,27 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.PubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.HMACGenerateKey(config.Random(), hash) + case packet.ExperimentalPubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.ExperimentalHMACGenerateKey(config.Random(), hash) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -326,7 +350,8 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // Generates an encryption/decryption key func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { - switch config.PublicKeyAlgorithm() { + pubKeyAlgo := config.PublicKeyAlgorithm() + switch pubKeyAlgo { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() if bits < 1024 { @@ -361,6 +386,33 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) + case packet.PubKeyAlgoHMAC, packet.PubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + aead := algorithm.AEADMode(config.AEAD().Mode()) + return symmetric.AEADGenerateKey(config.Random(), cipher, aead) + case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + return symmetric.ExperimentalAEADGenerateKey(config.Random(), cipher) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() && pubKeyAlgo == packet.PubKeyAlgoMlkem1024X448 { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem1024_x448 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/keys.go b/openpgp/keys.go index a071353e2..2517ff0ef 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -134,6 +134,7 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 + isPQ := false var maxTime time.Time for i, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && @@ -142,9 +143,10 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { !subkey.PublicKey.KeyExpired(subkey.Sig, now) && !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && - (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) { + (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime) || (!isPQ && subkey.IsPQ())) { candidateSubkey = i maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -201,6 +203,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 var maxTime time.Time + isPQ := false for idx, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && (flags&packet.KeyFlagCertify == 0 || subkey.Sig.FlagCertify) && @@ -210,9 +213,11 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() } } @@ -305,6 +310,11 @@ func (s *Subkey) Revoked(now time.Time) bool { return revoked(s.Revocations, now) } +// IsPQ returns true if the algorithm is Post-Quantum safe. +func (s *Subkey) IsPQ() bool { + return s.PublicKey.IsPQ() +} + // Revoked returns whether the key or subkey has been revoked by a self-signature. // Note that third-party revocation signatures are not supported. // Note also that Identity revocation should be checked separately. @@ -371,7 +381,7 @@ func (el EntityList) KeysByIdUsage(id uint64, requiredUsage byte) (keys []Key) { func (el EntityList) DecryptionKeys() (keys []Key) { for _, e := range el { for _, subKey := range e.Subkeys { - if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { + if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications || subKey.Sig.FlagForward) { keys = append(keys, Key{e, subKey.PublicKey, subKey.PrivateKey, subKey.Sig, subKey.Revocations}) } } @@ -761,6 +771,12 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo // Serialize writes the public part of the given Entity to w, including // signatures from other entities. No private key material will be output. func (e *Entity) Serialize(w io.Writer) error { + if e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoAEAD || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + return errors.InvalidArgumentError("Can't serialize symmetric primary key") + } err := e.PrimaryKey.Serialize(w) if err != nil { return err @@ -790,6 +806,18 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // The types of keys below are only useful as private keys. Thus, the + // public key packets contain no meaningful information and do not need + // to be serialized. + // Prevent public key export for forwarding keys, see forwarding section 4.1. + if subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoAEAD || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + subkey.Sig.FlagForward { + continue + } + err = subkey.PublicKey.Serialize(w) if err != nil { return err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 3cb4ac005..3ef6f0e04 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -1169,6 +1170,373 @@ func TestAddSubkeySerialized(t *testing.T) { } } +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.PubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 128 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 129 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.PubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { + t.Error("parsed wrong aead mode") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed + i = bytes.Index(w.Bytes(), firstFpSeed[:]) + + secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed + k = bytes.Index(w.Bytes(), secondFpSeed[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + +func TestAddExperimentalHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestSerializeExperimentalSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddExperimentalAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoExperimentalSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + func TestAddSubkeyWithConfig(t *testing.T) { c := &packet.Config{ DefaultHash: crypto.SHA512, @@ -1865,3 +2233,138 @@ mQ00BF00000BCAD0000000000000000000000000000000000000000000000000 000000000000000000000000000000000000ABE000G0Dn000000000000000000iQ00BB0BAgAGBCG00000` ReadArmoredKeyRing(strings.NewReader(data)) } + +func TestSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 +NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT +eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF +BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu +b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l +BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA +QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs +AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ +yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb +3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p +Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT +=ywfD +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.PubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.PubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} + +func TestExperimantalSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} + +func testAddMlkemSubkey(t *testing.T, entity *Entity, v6Keys bool) { + var err error + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + for name, algo := range asymmAlgos { + // Remove existing subkeys + entity.Subkeys = []Subkey{} + + t.Run(name, func(t *testing.T) { + kyberConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: v6Keys, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + err = entity.AddEncryptionSubkey(kyberConfig) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if entity.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + + if entity.Subkeys[0].PublicKey.Version != entity.PrivateKey.Version { + t.Fatalf("Expected subkey version: %d, got: %d", entity.PrivateKey.Version, + entity.Subkeys[0].PublicKey.Version) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(read.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if read.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + }) + } +} diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index fc9ba776d..7914d3eb0 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -3,8 +3,14 @@ package openpgp import ( "bytes" "crypto" + "crypto/rand" "strings" "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -196,3 +202,138 @@ func TestNewEntityWithDefaultHashv6(t *testing.T) { } } } + +func TestGeneratePqKey(t *testing.T) { + randomPassword := make([]byte, 128) + _, err := rand.Read(randomPassword) + if err != nil { + t.Fatal(err) + } + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for name, algo := range asymmAlgos { + t.Run(name, func(t *testing.T) { + config := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatalf("Failed to parse entity: %s", err) + } + + if read.PrimaryKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", algo, read.PrimaryKey.PubKeyAlgo) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err := read.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-DSA key was marked as invalid: ", err) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { + bin, err := pk.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + if pk.PublicMldsa, err = pk.Mldsa.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal(err) + } + } + + err = read.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-DSA key") + } + + testMlkemSubkey(t, read.Subkeys[0], randomPassword) + }) + } +} + +func testMlkemSubkey(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.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-KEM key was marked as invalid: ", err) + } + + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + // Corrupt public ML-KEM in primary key + if pk, ok := subkey.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey); ok { + bin, _ := pk.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + if pk.PublicMlkem, err = pk.Mlkem.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 ML-KEM key") + } +} + +func TestAddV6MlkemSubkey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + testAddMlkemSubkey(t, entity, true) +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go new file mode 100644 index 000000000..c7086144e --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -0,0 +1,119 @@ +// Package mldsa_eddsa implements hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-composite-signature-schemes +package mldsa_eddsa + +import ( + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" +) + +const ( + MlDsaSeedLen = 32 +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.EdDSACurve + Mldsa sign.Scheme + PublicPoint []byte + PublicMldsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMldsa sign.PrivateKey + SecretMldsaSeed []byte +} + +// GenerateKey generates a ML-DSA + EdDSA composite key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-generation-procedure-2 +func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mldsa = d + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateEdDSA(rand) + if err != nil { + return nil, err + } + + keySeed := make([]byte, d.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + + if err := priv.DeriveMlDsaKeys(keySeed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlDsaKeys derives the ML-DSA keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlDsaKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlDsaSeedLen { + return goerrors.New("mldsa_eddsa: ml-dsa secret seed has the wrong length") + } + priv.SecretMldsaSeed = seed + publicKey, privateKey := priv.PublicKey.Mldsa.DeriveKey(priv.SecretMldsaSeed) + if overridePublicKey { + priv.PublicKey.PublicMldsa = publicKey + } + priv.SecretMldsa = privateKey + return nil +} + +// Sign generates a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-signature-generation +func Sign(priv *PrivateKey, message []byte) (mldsaSig, ecSig []byte, err error) { + ecSig, err = priv.PublicKey.Curve.Sign(priv.PublicKey.PublicPoint, priv.SecretEc, message) + if err != nil { + return nil, nil, err + } + + // The default signer interface does not use the hedged variant. + // Thus, we need to use the low level api + switch key := priv.SecretMldsa.(type) { + case *mldsa65.PrivateKey: + mldsaSig = make([]byte, mldsa65.SignatureSize) + err = mldsa65.SignTo(key, message, nil, true, mldsaSig) + case *mldsa87.PrivateKey: + mldsaSig = make([]byte, mldsa87.SignatureSize) + err = mldsa87.SignTo(key, message, nil, true, mldsaSig) + default: + err = goerrors.New("mldsa_eddsa: unsupported ML-DSA private key type") + } + + if err != nil { + return nil, nil, err + } + + return mldsaSig, ecSig, nil +} + +// Verify verifies a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-signature-verification +func Verify(pub *PublicKey, message, dSig, ecSig []byte) bool { + return pub.Curve.Verify(pub.PublicPoint, message, ecSig) && pub.Mldsa.Verify(pub.PublicMldsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateEdDSA(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicMldsa.Equal(priv.SecretMldsa.Public()) { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + + return nil +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go new file mode 100644 index 000000000..20b9e1604 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -0,0 +1,95 @@ +// Package mldsa_eddsa_test tests the implementation of hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +package mldsa_eddsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA3_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicMldsa, err = key.Mldsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_eddsa.PrivateKey { + curveObj, err := packet.GetEdDSACurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMldsaFromAlgID(algId) + if err != nil { + t.Errorf("error getting ML-DSA: %s", err) + } + + priv, err := mldsa_eddsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *mldsa_eddsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, ecSig, err := mldsa_eddsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := mldsa_eddsa.Verify(&priv.PublicKey, digest, dSig, ecSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go new file mode 100644 index 000000000..cd9b5c0cb --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -0,0 +1,259 @@ +// Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +// It follows the spec https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-composite-kem-schemes +package mlkem_ecdh + +import ( + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "golang.org/x/crypto/sha3" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/kem" +) + +const ( + maxSessionKeyLength = 64 + MlKemSeedLen = 64 + domSep = "OpenPGPCompositeKDFv1" +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.ECDHCurve + Mlkem kem.Scheme + PublicMlkem kem.PublicKey + PublicPoint []byte +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMlkem kem.PrivateKey + SecretMlkemSeed []byte +} + +// GenerateKey implements ML-KEM + ECC key generation as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-generation-procedure +func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mlkem = k + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateECDH(rand) + if err != nil { + return nil, err + } + + seed, err := generateRandomSeed(rand, MlKemSeedLen) + if err != nil { + return nil, err + } + + if err := priv.DeriveMlKemKeys(seed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlKemKeys derives the ML-KEM keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlKemKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlKemSeedLen { + return goerrors.New("mlkem_ecdh: ml-kem secret seed has the wrong length") + } + priv.SecretMlkemSeed = seed + publicKey, privateKey := priv.PublicKey.Mlkem.DeriveKeyPair(priv.SecretMlkemSeed) + if overridePublicKey { + priv.PublicKey.PublicMlkem = publicKey + } + priv.SecretMlkem = privateKey + return nil +} + +// Encrypt implements ML-KEM + ECC encryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-encryption-procedure +func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { + if len(msg) > maxSessionKeyLength { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") + } + + if len(msg)%8 != 0 { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key not a multiple of 8") + } + + // EC shared secret derivation + ecEphemeral, ecSS, err := pub.Curve.Encaps(rand, pub.PublicPoint) + if err != nil { + return nil, nil, nil, err + } + + // ML-KEM shared secret derivation + kyberSeed, err := generateRandomSeed(rand, pub.Mlkem.EncapsulationSeedSize()) + if err != nil { + return nil, nil, nil, err + } + + kEphemeral, kSS, err := pub.Mlkem.EncapsulateDeterministically(pub.PublicMlkem, kyberSeed) + if err != nil { + return nil, nil, nil, err + } + + keyEncryptionKey, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS) + if err != nil { + return nil, nil, nil, err + } + + if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { + return nil, nil, nil, err + } + + return kEphemeral, ecEphemeral, ciphertext, nil +} + +// Decrypt implements ML-KEM + ECC decryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-decryption-procedure +func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg []byte, err error) { + // EC shared secret derivation + ecSS, err := priv.PublicKey.Curve.Decaps(ecEphemeral, priv.SecretEc) + if err != nil { + return nil, err + } + + // ML-KEM shared secret derivation + kSS, err := priv.PublicKey.Mlkem.Decapsulate(priv.SecretMlkem, kEphemeral) + if err != nil { + return nil, err + } + + kek, err := buildKey(&priv.PublicKey, ecSS, ecEphemeral, priv.PublicPoint, kSS) + if err != nil { + return nil, err + } + + return keywrap.Unwrap(kek, ciphertext) +} + +// buildKey implements the composite KDF from +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-combiner +func buildKey(pub *PublicKey, eccSecretPoint, eccCipherText, eccPublicKey, mlkemKeyShare []byte) ([]byte, error) { + /// Set the output `ecdhKeyShare` to `eccSecretPoint` + eccKeyShare := eccSecretPoint + + // mlkemKeyShare - the ML-KEM key share encoded as an octet string + // algId - the OpenPGP algorithm ID of the public-key encryption algorithm + // eccKeyShare - the ECDH key share encoded as an octet string + // eccCipherText - the ECDH ciphertext encoded as an octet string + // eccPublicKey - The ECDH public key of the recipient as an octet string + + // SHA3-256(mlkemKeyShare || eccKeyShare || eccCipherText || eccPublicKey || + // algId || "OpenPGPCompositeKDFv1" || len("OpenPGPCompositeKDFv1")) + h := sha3.New256() + _, _ = h.Write(mlkemKeyShare) + _, _ = h.Write(eccKeyShare) + _, _ = h.Write(eccCipherText) + _, _ = h.Write(eccPublicKey) + _, _ = h.Write([]byte{pub.AlgId}) + _, _ = h.Write([]byte(domSep)) + _, _ = h.Write([]byte{byte(len(domSep))}) + return h.Sum(nil), nil +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateECDH(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicKey.PublicMlkem.Equal(priv.SecretMlkem.Public()) { + return errors.KeyInvalidError("mlkem_ecdh: invalid public key") + } + + return +} + +// EncodeFields encodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(w io.Writer, ec, ml, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + if _, err = w.Write(ec); err != nil { + return err + } + + if _, err = w.Write(ml); err != nil { + return err + } + + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + + if _, err = w.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + + if !v6 { + if _, err = w.Write([]byte{cipherFunction}); err != nil { + return err + } + } + + if _, err = w.Write(encryptedSessionKey); err != nil { + return err + } + + return nil +} + +// DecodeFields decodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, encryptedMPI2, encryptedMPI3 encoding.Field, cipherFunction byte, err error) { + var buf [1]byte + + encryptedMPI1 = encoding.NewEmptyOctetArray(lenEcc) + if _, err = encryptedMPI1.ReadFrom(r); err != nil { + return + } + + encryptedMPI2 = encoding.NewEmptyOctetArray(lenMlkem) + if _, err = encryptedMPI2.ReadFrom(r); err != nil { + return + } + + // A one-octet size of the following fields. + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + cipherFunction = buf[0] + followingLen -= 1 + } + + // The encrypted session key. + encryptedMPI3 = encoding.NewEmptyOctetArray(int(followingLen)) + if _, err = encryptedMPI3.ReadFrom(r); err != nil { + return + } + + return +} + +func generateRandomSeed(rand io.Reader, size int) ([]byte, error) { + randomBytes := make([]byte, size) + if _, err := rand.Read(randomBytes); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + return randomBytes, nil +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go new file mode 100644 index 000000000..23a82eb36 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -0,0 +1,104 @@ +// Package mlkem_ecdh_test tests the implementation of hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +package mlkem_ecdh_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestEncryptDecrypt(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + symmAlgos := map[string]algorithm.Cipher{ + "AES-128": algorithm.AES128, + "AES-192": algorithm.AES192, + "AES-256": algorithm.AES256, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + for symmName, symmAlgo := range symmAlgos { + t.Run(symmName, func(t *testing.T) { + testEncryptDecryptAlgo(t, key, symmAlgo) + }) + } + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + var err error + key := testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, _ := key.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + key.PublicMlkem, err = key.Mlkem.UnmarshalBinaryPublicKey(bin) + if err != nil { + t.Fatal("unable to corrupt key") + } + + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_ecdh.PrivateKey { + curveObj, err := packet.GetECDHCurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMlkemFromAlgID(algId) + if err != nil { + t.Errorf("error getting kyber: %s", err) + } + + priv, err := mlkem_ecdh.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testEncryptDecryptAlgo(t *testing.T, priv *mlkem_ecdh.PrivateKey, kdfCipher algorithm.Cipher) { + expectedMessage := make([]byte, kdfCipher.KeySize()) // encryption algo + checksum + rand.Read(expectedMessage) + + kE, ecE, c, err := mlkem_ecdh.Encrypt(rand.Reader, &priv.PublicKey, expectedMessage) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + decryptedMessage, err := mlkem_ecdh.Decrypt(priv, kE, ecE, c) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + if !bytes.Equal(decryptedMessage, expectedMessage) { + t.Errorf("decryption failed, got: %x, want: %x", decryptedMessage, expectedMessage) + } +} diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index 142be0aa0..18536be73 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -447,8 +447,10 @@ func (c *Config) GenerateNonCriticalSignatureCreationTime() bool { } func (c *Config) DecompressedMessageSizeLimit() *int64 { - if c == nil { - return nil + if c == nil || c.MaxDecompressedMessageSize == nil { + // 50 MiB + max := 50 * (int64(1) << 20) + return &max } return c.MaxDecompressedMessageSize } diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index b90bb2891..c58aeee37 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -17,7 +17,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -33,10 +36,15 @@ type EncryptedKey struct { CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt - encryptedMPI1, encryptedMPI2 encoding.Field - ephemeralPublicX25519 *x25519.PublicKey // used for x25519 - ephemeralPublicX448 *x448.PublicKey // used for x448 - encryptedSession []byte // used for x25519 and x448 + encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, AEAD and PQC keys + encryptedMPI2 encoding.Field // Only valid in Elgamal, ECDH and PQC keys + encryptedMPI3 encoding.Field // Only valid in PQC keys + ephemeralPublicX25519 *x25519.PublicKey // used for x25519 + ephemeralPublicX448 *x448.PublicKey // used for x448 + encryptedSession []byte // used for x25519 and x448 + + nonce []byte + aeadMode algorithm.AEADMode } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -133,12 +141,41 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { if err != nil { return } + case PubKeyAlgoAEAD: + ivAndCiphertext, err := io.ReadAll(r) + if err != nil { + return err + } + e.encryptedMPI1 = encoding.NewOctetArray(ivAndCiphertext) + case ExperimentalPubKeyAlgoAEAD: + var aeadMode [1]byte + if _, err = readFull(r, aeadMode[:]); err != nil { + return + } + e.aeadMode = algorithm.AEADMode(aeadMode[0]) + nonceLength := e.aeadMode.NonceLength() + e.nonce = make([]byte, nonceLength) + if _, err = readFull(r, e.nonce); err != nil { + return + } + e.encryptedMPI1 = new(encoding.ShortByteString) + if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { + return + } + case PubKeyAlgoMlkem768X25519: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 32, 1088, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem1024X448: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 56, 1568, e.Version == 6); err != nil { + return err + } } if e.Version < 6 { switch e.Algo { - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: e.CipherFunc = CipherFunction(cipherFunction) - // Check for validiy is in the Decrypt method + // Check for validity is in the Decrypt method } } @@ -191,6 +228,18 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { b, err = x25519.Decrypt(priv.PrivateKey.(*x25519.PrivateKey), e.ephemeralPublicX25519, e.encryptedSession) case PubKeyAlgoX448: b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession) + case PubKeyAlgoAEAD: + priv := priv.PrivateKey.(*symmetric.AEADPrivateKey) + b, err = priv.Decrypt(e.encryptedMPI1.Bytes(), priv.PublicKey.AEADMode) + case ExperimentalPubKeyAlgoAEAD: + priv := priv.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey) + b, err = priv.Decrypt(e.nonce, e.encryptedMPI1.Bytes(), e.aeadMode) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + ecE := e.encryptedMPI1.Bytes() + kE := e.encryptedMPI2.Bytes() + m := e.encryptedMPI3.Bytes() + + b, err = mlkem_ecdh.Decrypt(priv.PrivateKey.(*mlkem_ecdh.PrivateKey), kE, ecE, m) default: err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } @@ -200,7 +249,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { var key []byte switch priv.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoAEAD, ExperimentalPubKeyAlgoAEAD: keyOffset := 0 if e.Version < 6 { e.CipherFunc = CipherFunction(b[0]) @@ -210,22 +259,22 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } } key, err = decodeChecksumKey(b[keyOffset:]) - if err != nil { - return err - } - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: if e.Version < 6 { switch e.CipherFunc { case CipherAES128, CipherAES192, CipherAES256: break default: - return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448") + return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519, x448, and PQC") } } key = b[:] default: return errors.UnsupportedError("unsupported algorithm for decryption") } + if err != nil { + return err + } e.Key = key return nil } @@ -244,6 +293,11 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6) case PubKeyAlgoX448: encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + int(e.encryptedMPI3.EncodedLength()) + 1 + if e.Version < 6 { + encodedLength += 1 + } default: return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo))) } @@ -314,6 +368,9 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { case PubKeyAlgoX448: err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6) return err + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + err := mlkem_ecdh.EncodeFields(w, e.encryptedMPI1.EncodedBytes(), e.encryptedMPI2.EncodedBytes(), e.encryptedMPI3.EncodedBytes(), byte(e.CipherFunc), e.Version == 6) + return err default: panic("internal error") } @@ -346,13 +403,13 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal { return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed") } - // In v3 PKESKs, for x25519 and x448, mandate using AES - if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) { - switch cipherFunc { - case CipherAES128, CipherAES192, CipherAES256: - break + // In v3 PKESKs, for X25519 and X448, mandate using AES + if version == 3 && cipherFunc != CipherAES128 && cipherFunc != CipherAES192 && cipherFunc != CipherAES256 { + switch pub.PubKeyAlgo { + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519, x448, and PQC") default: - return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448") + break } } @@ -389,7 +446,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph var keyBlock []byte switch pub.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoAEAD, ExperimentalPubKeyAlgoAEAD: lenKeyBlock := len(key) + 2 if version < 6 { lenKeyBlock += 1 // cipher type included @@ -401,7 +458,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: // algorithm is added in plaintext below keyBlock = key } @@ -417,7 +474,13 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoX448: return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) - case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly: + case PubKeyAlgoAEAD: + return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock) + case ExperimentalPubKeyAlgoAEAD: + return serializeEncryptedKeyExperimentalAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.ExperimentalAEADPublicKey), keyBlock, config.AEAD()) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return serializeEncryptedKeyMlkem(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*mlkem_ecdh.PublicKey), keyBlock, byte(cipherFunc), version) + case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly, ExperimentalPubKeyAlgoHMAC: return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo))) } @@ -442,6 +505,36 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } +func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { + if e.Algo != PubKeyAlgoECDH { + return nil, errors.InvalidArgumentError("invalid PKESK") + } + + if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { + return nil, errors.InvalidArgumentError("invalid key id in PKESK") + } + + ephemeral := e.encryptedMPI1.Bytes() + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, instance.ProxyParameter) + if err != nil { + return nil, err + } + + wrappedKey := e.encryptedMPI2.Bytes() + copiedWrappedKey := make([]byte, len(wrappedKey)) + copy(copiedWrappedKey, wrappedKey) + + transformed = &EncryptedKey{ + Version: e.Version, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), + } + + return transformed, nil +} + func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) if err != nil { @@ -558,6 +651,65 @@ func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub * return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) } +func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte) error { + mode := pub.AEADMode + iv, ciphertext, err := pub.Encrypt(rand, keyBlock, mode) + if err != nil { + return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) + } + + packetLen := len(header) /* header length */ + packetLen += int(len(iv)) + packetLen += int(len(ciphertext)) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + + _, err = w.Write(iv[:]) + if err != nil { + return err + } + + _, err = w.Write(ciphertext) + return err +} + +func serializeEncryptedKeyExperimentalAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.ExperimentalAEADPublicKey, keyBlock []byte, config *AEADConfig) error { + mode := algorithm.AEADMode(config.Mode()) + iv, ciphertextRaw, err := pub.Encrypt(rand, keyBlock, mode) + if err != nil { + return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) + } + + ciphertextShortByteString := encoding.NewShortByteString(ciphertextRaw) + + buffer := append([]byte{byte(mode)}, iv...) + buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) + + packetLen := len(header) /* header length */ + packetLen += int(len(buffer)) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + + _, err = w.Write(buffer) + return err +} + func checksumKeyMaterial(key []byte) uint16 { var checksum uint16 for _, v := range key { @@ -582,3 +734,32 @@ func encodeChecksumKey(buffer []byte, key []byte) { buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } + +func serializeEncryptedKeyMlkem(w io.Writer, rand io.Reader, header []byte, pub *mlkem_ecdh.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + mlE, ecE, c, err := mlkem_ecdh.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("ML-KEM + ECDH encryption failed: " + err.Error()) + } + + ml := encoding.NewOctetArray(mlE) + ec := encoding.NewOctetArray(ecE) + m := encoding.NewOctetArray(c) + + packetLen := len(header) /* header length */ + packetLen += int(ec.EncodedLength()) + int(ml.EncodedLength()) + int(m.EncodedLength()) + 1 + if version < 6 { + packetLen += 1 + } + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header) + if err != nil { + return err + } + + return mlkem_ecdh.EncodeFields(w, ec.EncodedBytes(), ml.EncodedBytes(), m.EncodedBytes(), cipherFunc, version == 6) +} diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index 787c7feca..1fde89c3a 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -16,6 +16,7 @@ import ( "crypto" "crypto/rsa" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -338,3 +339,53 @@ func TestSerializingEncryptedKey(t *testing.T) { t.Fatalf("serialization of encrypted key differed from original. Original was %s, but reserialized as %s", encryptedKeyHex, bufHex) } } + +func TestSymmetricallyEncryptedKey(t *testing.T) { + const encryptedKeyHex = "c13d03999bd17d726446da80df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + + expectedIvAndCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + + p, err := Read(readerFromHex(encryptedKeyHex)) + if err != nil { + t.Fatal("error reading packet") + } + + ek, ok := p.(*EncryptedKey) + if !ok { + t.Fatalf("didn't parse and EncryptedKey, got %#v", p) + } + + if !bytes.Equal(expectedIvAndCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedIvAndCiphertext) + } +} + +func TestExperimentalSymmetricallyEncryptedKey(t *testing.T) { + const encryptedKeyHex = "c14f03999bd17d726446da64018cb4d628ae753c646b81f87f21269cd733df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + + expectedNonce := []byte{0x8c, 0xb4, 0xd6, 0x28, 0xae, 0x75, 0x3c, 0x64, 0x6b, 0x81, 0xf8, 0x7f, 0x21, 0x26, 0x9c, 0xd7} + + expectedCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + + p, err := Read(readerFromHex(encryptedKeyHex)) + if err != nil { + t.Fatal("error reading packet") + } + + ek, ok := p.(*EncryptedKey) + if !ok { + t.Fatalf("didn't parse and EncryptedKey, got %#v", p) + } + + if ek.aeadMode != algorithm.AEADModeEAX { + t.Errorf("Parsed wrong aead mode, got %d, expected: 1", ek.aeadMode) + } + + if !bytes.Equal(expectedNonce, ek.nonce) { + t.Errorf("Parsed wrong nonce, got %x, expected %x", ek.nonce, expectedNonce) + } + + if !bytes.Equal(expectedCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedCiphertext) + } +} diff --git a/openpgp/packet/forwarding.go b/openpgp/packet/forwarding.go new file mode 100644 index 000000000..f16a2fbdc --- /dev/null +++ b/openpgp/packet/forwarding.go @@ -0,0 +1,36 @@ +package packet + +import "encoding/binary" + +// ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) +type ForwardingInstance struct { + KeyVersion int + ForwarderFingerprint []byte + ForwardeeFingerprint []byte + ProxyParameter []byte +} + +func (f *ForwardingInstance) GetForwarderKeyId() uint64 { + return computeForwardingKeyId(f.ForwarderFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) GetForwardeeKeyId() uint64 { + return computeForwardingKeyId(f.ForwardeeFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) getForwardeeKeyIdOrZero(originalKeyId uint64) uint64 { + if originalKeyId == 0 { + return 0 + } + + return f.GetForwardeeKeyId() +} + +func computeForwardingKeyId(fingerprint []byte, version int) uint64 { + switch version { + case 4: + return binary.BigEndian.Uint64(fingerprint[12:20]) + default: + panic("invalid pgp key version") + } +} diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 1e92e22c9..92be5773c 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -506,16 +506,32 @@ const ( PubKeyAlgoEd25519 PublicKeyAlgorithm = 27 PubKeyAlgoEd448 PublicKeyAlgorithm = 28 + PubKeyAlgoAEAD PublicKeyAlgorithm = 128 + PubKeyAlgoHMAC PublicKeyAlgorithm = 129 + ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 100 + ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 101 + // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 PubKeyAlgoRSASignOnly PublicKeyAlgorithm = 3 + + // PQC DSA algorithms + PubKeyAlgoMldsa65Ed25519 = 30 + PubKeyAlgoMldsa87Ed448 = 31 + + // PQC KEM algorithms + PubKeyAlgoMlkem768X25519 = 35 + PubKeyAlgoMlkem1024X448 = 36 ) // CanEncrypt returns true if it's possible to encrypt a message to a public // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, + PubKeyAlgoX25519, PubKeyAlgoX448, + PubKeyAlgoAEAD, ExperimentalPubKeyAlgoAEAD, + PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return true } return false @@ -525,7 +541,10 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { // sign a message. func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, + PubKeyAlgoEd25519, PubKeyAlgoEd448, + PubKeyAlgoHMAC, ExperimentalPubKeyAlgoHMAC, + PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index f04e6c6b8..7ca1a3e34 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -13,6 +13,7 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" + goerrors "errors" "fmt" "io" "math/big" @@ -27,7 +28,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "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/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" "golang.org/x/crypto/hkdf" @@ -166,6 +170,12 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) case ed448.PrivateKey: pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) + case *symmetric.HMACPrivateKey: + pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) + case *symmetric.ExperimentalHMACPrivateKey: + pk.PublicKey = *NewExperimentalHMACPublicKey(creationTime, &pubkey.PublicKey) + case *mldsa_eddsa.PrivateKey: + pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -173,7 +183,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey return pk } -// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey. +// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448|mlkem_ecdh}.PrivateKey. func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey { pk := new(PrivateKey) switch priv := decrypter.(type) { @@ -187,6 +197,12 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) case *x448.PrivateKey: pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) + case *symmetric.AEADPrivateKey: + pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) + case *symmetric.ExperimentalAEADPrivateKey: + pk.PublicKey = *NewExperimentalAEADPublicKey(creationTime, &priv.PublicKey) + case *mlkem_ecdh.PrivateKey: + pk.PublicKey = *NewMlkemEcdhPublicKey(creationTime, &priv.PublicKey) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -530,6 +546,56 @@ func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error { return err } +func serializeAEADPrivateKey(w io.Writer, priv *symmetric.AEADPrivateKey) (err error) { + _, err = w.Write(priv.Key) + return +} + +func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err error) { + _, err = w.Write(priv.Key) + return err +} + +func serializeExperimentalAEADPrivateKey(w io.Writer, priv *symmetric.ExperimentalAEADPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } + _, err = w.Write(priv.Key) + return +} + +func serializeExperimentalHMACPrivateKey(w io.Writer, priv *symmetric.ExperimentalHMACPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } + _, err = w.Write(priv.Key) + return err +} + +// serializeMlkemPrivateKey serializes a ML-KEM + ECC private key according to +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets +func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err error) { + if _, err = w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + _, err = w.Write(encoding.NewOctetArray(priv.SecretMlkemSeed).EncodedBytes()) + return err +} + +// serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets-2 +func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) error { + if _, err := w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + if _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsaSeed).EncodedBytes()); 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() { @@ -830,6 +896,18 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeEd25519PrivateKey(w, priv) case *ed448.PrivateKey: err = serializeEd448PrivateKey(w, priv) + case *symmetric.AEADPrivateKey: + err = serializeAEADPrivateKey(w, priv) + case *symmetric.HMACPrivateKey: + err = serializeHMACPrivateKey(w, priv) + case *symmetric.ExperimentalAEADPrivateKey: + err = serializeExperimentalAEADPrivateKey(w, priv) + case *symmetric.ExperimentalHMACPrivateKey: + err = serializeExperimentalHMACPrivateKey(w, priv) + case *mlkem_ecdh.PrivateKey: + err = serializeMlkemPrivateKey(w, priv) + case *mldsa_eddsa.PrivateKey: + err = serializeMldsaEddsaPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -858,6 +936,28 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseEd25519PrivateKey(data) case PubKeyAlgoEd448: return pk.parseEd448PrivateKey(data) + case PubKeyAlgoAEAD: + return pk.parseAEADPrivateKey(data) + case PubKeyAlgoHMAC: + return pk.parseHMACPrivateKey(data) + case ExperimentalPubKeyAlgoAEAD: + return pk.parseExperimentalAEADPrivateKey(data) + case ExperimentalPubKeyAlgoHMAC: + return pk.parseExperimentalHMACPrivateKey(data) + case PubKeyAlgoMlkem768X25519: + if !(pk.Version == 4 || pk.Version >= 6) { + return goerrors.New("openpgp: ML-KEM-768+X25519 may only be used with v4 or v6+") + } + return pk.parseMlkemEcdhPrivateKey(data, 32, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMlkem1024X448: + if pk.Version < 6 { + return goerrors.New("openpgp: ML-KEM-1024+X448 may only be used with v6+") + } + return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMldsa65Ed25519: + return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa_eddsa.MlDsaSeedLen) + case PubKeyAlgoMldsa87Ed448: + return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa_eddsa.MlDsaSeedLen) default: err = errors.StructuralError("unknown private key type") return @@ -1121,6 +1221,161 @@ func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte { return encryptionKey } +func (pk *PrivateKey) parseAEADPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + aeadPriv := new(symmetric.AEADPrivateKey) + aeadPriv.PublicKey = *pubKey + + priv := make([]byte, pubKey.Cipher.KeySize()) + copy(priv, data[:]) + aeadPriv.Key = priv + aeadPriv.PublicKey.Key = aeadPriv.Key + + pk.PrivateKey = aeadPriv + pk.PublicKey.PublicKey = &aeadPriv.PublicKey + return +} + +func (pk *PrivateKey) parseHMACPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + hmacPriv := new(symmetric.HMACPrivateKey) + hmacPriv.PublicKey = *pubKey + + priv := make([]byte, pubKey.Hash.Size()) + copy(priv, data[:]) + hmacPriv.Key = priv[:] + hmacPriv.PublicKey.Key = hmacPriv.Key + + pk.PrivateKey = hmacPriv + pk.PublicKey.PublicKey = &hmacPriv.PublicKey + return +} + +func (pk *PrivateKey) parseExperimentalAEADPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + + aeadPriv := new(symmetric.ExperimentalAEADPrivateKey) + aeadPriv.PublicKey = *pubKey + + copy(aeadPriv.HashSeed[:], data[:32]) + + priv := make([]byte, pubKey.Cipher.KeySize()) + copy(priv, data[32:]) + aeadPriv.Key = priv + aeadPriv.PublicKey.Key = aeadPriv.Key + + if err = validateExperimentalAEADParameters(aeadPriv); err != nil { + return + } + + pk.PrivateKey = aeadPriv + pk.PublicKey.PublicKey = &aeadPriv.PublicKey + return +} + +func (pk *PrivateKey) parseExperimentalHMACPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + + hmacPriv := new(symmetric.ExperimentalHMACPrivateKey) + hmacPriv.PublicKey = *pubKey + + copy(hmacPriv.HashSeed[:], data[:32]) + + priv := make([]byte, pubKey.Hash.Size()) + copy(priv, data[32:]) + hmacPriv.Key = data[32:] + hmacPriv.PublicKey.Key = hmacPriv.Key + + if err = validateExperimentalHMACParameters(hmacPriv); err != nil { + return + } + + pk.PrivateKey = hmacPriv + pk.PublicKey.PublicKey = &hmacPriv.PublicKey + return +} + +func validateExperimentalAEADParameters(priv *symmetric.ExperimentalAEADPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateExperimentalHMACParameters(priv *symmetric.ExperimentalHMACPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { + expectedBindingHash := symmetric.ComputeBindingHash(seed) + if !bytes.Equal(expectedBindingHash, bindingHash[:]) { + return errors.KeyInvalidError("symmetric: wrong binding hash") + } + return nil +} + +// parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets-2 +func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, seedLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + EdDSA key") + } + pub := pk.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey) + priv := new(mldsa_eddsa.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlDsaKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mldsa_eddsa.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseMlkemEcdhPrivateKey parses a ML-KEM + ECC private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets +func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, seedLen int) (err error) { + pub := pk.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey) + priv := new(mlkem_ecdh.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlKemKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mlkem_ecdh.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 e2813396e..ff55c8941 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,12 +5,14 @@ package packet import ( + "bytes" "crypto/dsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" _ "crypto/sha512" "encoding/binary" + goerrors "errors" "fmt" "hash" "io" @@ -28,8 +30,17 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "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/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" + "github.com/cloudflare/circl/kem" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. @@ -37,7 +48,7 @@ type PublicKey struct { Version int CreationTime time.Time PubKeyAlgo PublicKeyAlgorithm - PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey + PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey, or *mlkem_ecdh.PublicKey Fingerprint []byte KeyId uint64 IsSubkey bool @@ -69,6 +80,26 @@ func (pk *PublicKey) UpgradeToV6() error { return pk.checkV6Compatibility() } +// ReplaceKDF replaces the KDF instance, and updates all necessary fields. +func (pk *PublicKey) ReplaceKDF(kdf ecdh.KDF) error { + ecdhKey, ok := pk.PublicKey.(*ecdh.PublicKey) + if !ok { + return goerrors.New("wrong forwarding sub key generation") + } + + ecdhKey.KDF = kdf + byteBuffer := new(bytes.Buffer) + err := kdf.Serialize(byteBuffer) + if err != nil { + return err + } + + pk.kdf = encoding.NewOID(byteBuffer.Bytes()[1:]) + pk.setFingerprintAndKeyId() + + return nil +} + // signingKey provides a convenient abstraction over signature verification // for v3 and v4 public keys. type signingKey interface { @@ -230,6 +261,87 @@ func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey return pk } +func NewAEADPublicKey(creationTime time.Time, pub *symmetric.AEADPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoAEAD, + PublicKey: pub, + } + + return pk +} + +func NewHMACPublicKey(creationTime time.Time, pub *symmetric.HMACPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoHMAC, + PublicKey: pub, + } + + return pk +} + +func NewExperimentalAEADPublicKey(creationTime time.Time, pub *symmetric.ExperimentalAEADPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoAEAD, + PublicKey: pub, + } + + return pk +} + +func NewExperimentalHMACPublicKey(creationTime time.Time, pub *symmetric.ExperimentalHMACPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoHMAC, + PublicKey: pub, + } + + return pk +} + +func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *PublicKey { + mlkemBin, err := pub.PublicMlkem.MarshalBinary() + if err != nil { + panic(err) + } + + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(mlkemBin), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) *PublicKey { + publicKeyBytes, err := pub.PublicMldsa.MarshalBinary() + if err != nil { + panic(err) + } + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + 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 @@ -258,7 +370,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { } pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0) pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5]) - // Ignore four-ocet length + // Ignore four-octet length switch pk.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: err = pk.parseRSA(r) @@ -280,6 +392,22 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseEd25519(r) case PubKeyAlgoEd448: err = pk.parseEd448(r) + case PubKeyAlgoAEAD: + err = pk.parseAEAD(r) + case PubKeyAlgoHMAC: + err = pk.parseHMAC(r) + case ExperimentalPubKeyAlgoAEAD: + err = pk.parseExperimentalAEAD(r) + case ExperimentalPubKeyAlgoHMAC: + err = pk.parseExperimentalHMAC(r) + case PubKeyAlgoMlkem768X25519: + err = pk.parseMlkemEcdh(r, 32, mlkem768.PublicKeySize) + case PubKeyAlgoMlkem1024X448: + err = pk.parseMlkemEcdh(r, 56, mlkem1024.PublicKeySize) + case PubKeyAlgoMldsa65Ed25519: + err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize) + case PubKeyAlgoMldsa87Ed448: + err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -474,11 +602,13 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid)) } - if kdfLen := len(pk.kdf.Bytes()); kdfLen < 3 { + kdfLen := len(pk.kdf.Bytes()) + if kdfLen < 3 { return errors.UnsupportedError("unsupported ECDH KDF length: " + strconv.Itoa(kdfLen)) } - if reserved := pk.kdf.Bytes()[0]; reserved != 0x01 { - return errors.UnsupportedError("unsupported KDF reserved field: " + strconv.Itoa(int(reserved))) + kdfVersion := int(pk.kdf.Bytes()[0]) + if kdfVersion != ecdh.KDFVersion1 && kdfVersion != ecdh.KDFVersionForwarding { + return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(kdfVersion)) } kdfHash, ok := algorithm.HashById[pk.kdf.Bytes()[1]] if !ok { @@ -489,9 +619,57 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError("unsupported ECDH KDF cipher: " + strconv.Itoa(int(pk.kdf.Bytes()[2]))) } - ecdhKey := ecdh.NewPublicKey(c, kdfHash, kdfCipher) + kdf := ecdh.KDF{ + Version: kdfVersion, + Hash: kdfHash, + Cipher: kdfCipher, + } + + if kdfVersion == ecdh.KDFVersionForwarding { + if pk.Version != 4 || kdfLen != 23 { + return errors.UnsupportedError("unsupported ECDH KDF v2 length: " + strconv.Itoa(kdfLen)) + } + + kdf.ReplacementFingerprint = pk.kdf.Bytes()[3:23] + } + + ecdhKey := ecdh.NewPublicKey(c, kdf) err = ecdhKey.UnmarshalPoint(pk.p.Bytes()) pk.PublicKey = ecdhKey + return +} + +// parseMlkemEcdh parses a ML-KEM + ECC public key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets +func (pk *PublicKey) parseMlkemEcdh(r io.Reader, ecLen, kLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(kLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mlkem_ecdh.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetECDHCurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mlkem, err = GetMlkemFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMlkem, err = pub.Mlkem.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub return } @@ -594,6 +772,141 @@ func (pk *PublicKey) parseEd448(r io.Reader) (err error) { return } +func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { + var algOctets [2]byte + _, err = readFull(r, algOctets[:]) + if err != nil { + return + } + + var fpSeed [32]byte + _, err = readFull(r, fpSeed[:]) + if err != nil { + return + } + + symmetric := &symmetric.AEADPublicKey{ + Cipher: algorithm.CipherFunction(algOctets[0]), + AEADMode: algorithm.AEADMode(algOctets[1]), + FpSeed: fpSeed, + } + + pk.PublicKey = symmetric + return +} + +func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { + var hash [1]byte + _, err = readFull(r, hash[:]) + if err != nil { + return + } + var fpSeed [32]byte + _, err = readFull(r, fpSeed[:]) + if err != nil { + return + } + + hmacHash, ok := algorithm.HashById[hash[0]] + if !ok { + return errors.UnsupportedError("unsupported HMAC hash: " + strconv.Itoa(int(hash[0]))) + } + + symmetric := &symmetric.HMACPublicKey{ + Hash: hmacHash, + FpSeed: fpSeed, + } + + pk.PublicKey = symmetric + return +} + +func (pk *PublicKey) parseExperimentalAEAD(r io.Reader) (err error) { + var cipher [1]byte + _, err = readFull(r, cipher[:]) + if err != nil { + return + } + + var bindingHash [32]byte + _, err = readFull(r, bindingHash[:]) + if err != nil { + return + } + + symmetric := &symmetric.ExperimentalAEADPublicKey{ + Cipher: algorithm.CipherFunction(cipher[0]), + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func (pk *PublicKey) parseExperimentalHMAC(r io.Reader) (err error) { + var hash [1]byte + _, err = readFull(r, hash[:]) + if err != nil { + return + } + bindingHash, err := readBindingHash(r) + if err != nil { + return + } + + hmacHash, ok := algorithm.HashById[hash[0]] + if !ok { + return errors.UnsupportedError("unsupported HMAC hash: " + strconv.Itoa(int(hash[0]))) + } + + symmetric := &symmetric.ExperimentalHMACPublicKey{ + Hash: hmacHash, + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { + _, err = readFull(r, bindingHash[:]) + return bindingHash, err +} + +// parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-key-material-packets-2 +func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(dLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mldsa_eddsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetEdDSACurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMldsa, err = pub.Mldsa.UnmarshalBinaryPublicKey(pk.q.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 { @@ -681,6 +994,19 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length += ed25519.PublicKeySize case PubKeyAlgoEd448: length += ed448.PublicKeySize + case PubKeyAlgoAEAD: + length += 2 // Symmetric and AEAD algorithm octets + length += 32 // Fingerprint seed + case PubKeyAlgoHMAC: + length += 1 // Hash octet + length += 32 // Fingerprint seed + case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: + length += 1 // Hash octet + length += 32 // Binding hash + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.q.EncodedLength()) default: panic("unknown public key algorithm") } @@ -773,13 +1099,52 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { publicKey := pk.PublicKey.(*ed448.PublicKey) _, err = w.Write(publicKey.Point) return + case PubKeyAlgoAEAD: + symmKey := pk.PublicKey.(*symmetric.AEADPublicKey) + algOctets := [2]byte{symmKey.Cipher.Id(), symmKey.AEADMode.Id()} + if _, err = w.Write(algOctets[:]); err != nil { + return + } + _, err = w.Write(symmKey.FpSeed[:]) + return + case PubKeyAlgoHMAC: + symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) + hashOctet := [1]byte{symmKey.Hash.Id()} + if _, err = w.Write(hashOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.FpSeed[:]) + return + case ExperimentalPubKeyAlgoAEAD: + symmKey := pk.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + cipherOctet := [1]byte{symmKey.Cipher.Id()} + if _, err = w.Write(cipherOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return + case ExperimentalPubKeyAlgoHMAC: + symmKey := pk.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + hashOctet := [1]byte{symmKey.Hash.Id()} + if _, err = w.Write(hashOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(pk.p.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.q.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } // CanSign returns true iff this public key can generate signatures func (pk *PublicKey) CanSign() bool { - return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH + return pk.PubKeyAlgo.CanSign() } // VerifyHashTag returns nil iff sig appears to be a plausible signature of the data @@ -859,6 +1224,34 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("ed448 verification failure") } return nil + case PubKeyAlgoHMAC: + HMACKey := pk.PublicKey.(*symmetric.HMACPublicKey) + + result, err := HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) + if err != nil { + return err + } + if !result { + return errors.SignatureError("HMAC verification failure") + } + return nil + case ExperimentalPubKeyAlgoHMAC: + HMACKey := pk.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + + result, err := HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) + if err != nil { + return err + } + if !result { + return errors.SignatureError("HMAC verification failure") + } + return nil + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + mldsaEddsaPublicKey := pk.PublicKey.(*mldsa_eddsa.PublicKey) + if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.EdDSASigR.Bytes()) { + return errors.SignatureError("MldsaEddsa verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -929,6 +1322,13 @@ func (pk *PublicKey) VerifyKeySignature(signed *PublicKey, sig *Signature) error } } + // Keys having this flag MUST have the forwarding KDF parameters version 2 defined in Section 5.1. + if sig.FlagForward && (signed.PubKeyAlgo != PubKeyAlgoECDH || + signed.kdf == nil || + signed.kdf.Bytes()[0] != ecdh.KDFVersionForwarding) { + return errors.StructuralError("forwarding key with wrong ecdh kdf version") + } + return nil } @@ -1085,6 +1485,13 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed25519.PublicKeySize * 8 case PubKeyAlgoEd448: bitLength = ed448.PublicKeySize * 8 + case PubKeyAlgoAEAD: + bitLength = uint16(pk.PublicKey.(*symmetric.AEADPublicKey).Cipher.KeySize()) * 8 + case ExperimentalPubKeyAlgoAEAD: + bitLength = 32 + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1123,3 +1530,71 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second) return currentTime.Unix() > expiry.Unix() } + +// IsPQ returns true if the algorithm of this public key is Post-Quantum safe. +func (pg *PublicKey) IsPQ() bool { + switch pg.PubKeyAlgo { + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, + PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } +} + +func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return PubKeyAlgoMlkem768X25519, nil + case PubKeyAlgoMldsa87Ed448: + return PubKeyAlgoMlkem1024X448, nil + default: + return 0, goerrors.New("packet: unsupported pq public key algorithm") + } +} + +// GetMlkemFromAlgID returns the ML-KEM instance from the matching KEM +func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return mlkem768.Scheme(), nil + case PubKeyAlgoMlkem1024X448: + return mlkem1024.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-KEM public key algorithm") + } +} + +// GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM +func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return ecc.NewCurve25519(), nil + case PubKeyAlgoMlkem1024X448: + return ecc.NewX448(), nil + default: + return nil, goerrors.New("packet: unsupported ECDH public key algorithm") + } +} + +func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return ecc.NewEd25519(), nil + case PubKeyAlgoMldsa87Ed448: + return ecc.NewEd448(), nil + default: + return nil, goerrors.New("packet: unsupported EdDSA public key algorithm") + } +} + +func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return mldsa65.Scheme(), nil + case PubKeyAlgoMldsa87Ed448: + return mldsa87.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-DSA public key algorithm") + } +} diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 4490fdf83..6986389c2 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -23,6 +23,9 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "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/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) const ( @@ -34,7 +37,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -80,7 +83,10 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field + HMAC encoding.Field EdSig []byte + MldsaSig encoding.Field + SlhdsaSig encoding.Field // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -127,8 +133,9 @@ type Signature struct { // FlagsValid is set if any flags were given. See RFC 9580, section // 5.2.3.29 for details. - FlagsValid bool - FlagCertify, FlagSign, FlagEncryptCommunications, FlagEncryptStorage, FlagSplitKey, FlagAuthenticate, FlagGroupKey bool + FlagsValid bool + FlagCertify, FlagSign, FlagEncryptCommunications, FlagEncryptStorage bool + FlagSplitKey, FlagAuthenticate, FlagForward, FlagGroupKey bool // RevocationReason is set if this signature has been revoked. // See RFC 9580, section 5.2.3.31 for details. @@ -198,7 +205,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { sig.SigType = SignatureType(buf[0]) sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, + PubKeyAlgoEd448, PubKeyAlgoHMAC, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -336,12 +344,44 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err != nil { return } + case PubKeyAlgoHMAC: + hmac, err := io.ReadAll(r) + if err != nil { + return err + } + sig.HMAC = encoding.NewOctetArray(hmac) + case ExperimentalPubKeyAlgoHMAC: + sig.HMAC = new(encoding.ShortByteString) + if _, err = sig.HMAC.ReadFrom(r); err != nil { + return + } + case PubKeyAlgoMldsa65Ed25519: + if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { + return + } + case PubKeyAlgoMldsa87Ed448: + if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { + return + } default: panic("unreachable") } return } +// parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-09.html#name-signature-packet-tag-2 +func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (err error) { + sig.EdDSASigR = encoding.NewEmptyOctetArray(ecLen) + if _, err = sig.EdDSASigR.ReadFrom(r); err != nil { + return + } + + sig.MldsaSig = encoding.NewEmptyOctetArray(dLen) + _, err = sig.MldsaSig.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) { @@ -582,6 +622,9 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r if subpacket[0]&KeyFlagAuthenticate != 0 { sig.FlagAuthenticate = true } + if subpacket[0]&KeyFlagForward != 0 { + sig.FlagForward = true + } if subpacket[0]&KeyFlagGroupKey != 0 { sig.FlagGroupKey = true } @@ -996,6 +1039,27 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e if err == nil { sig.EdSig = signature } + case PubKeyAlgoHMAC: + sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, nil) + if err == nil { + sig.HMAC = encoding.NewOctetArray(sigdata) + } + case ExperimentalPubKeyAlgoHMAC: + sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, nil) + if err == nil { + sig.HMAC = encoding.NewShortByteString(sigdata) + } + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if sig.Version != 6 { + return errors.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + sk := priv.PrivateKey.(*mldsa_eddsa.PrivateKey) + dSig, ecSig, err := mldsa_eddsa.Sign(sk, digest) + + if err == nil { + sig.MldsaSig = encoding.NewOctetArray(dSig) + sig.EdDSASigR = encoding.NewOctetArray(ecSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1113,7 +1177,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 && sig.EdSig == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.SlhdsaSig == nil && sig.HMAC == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -1134,6 +1198,11 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = ed25519.SignatureSize case PubKeyAlgoEd448: sigLength = ed448.SignatureSize + case ExperimentalPubKeyAlgoHMAC: + sigLength = int(sig.HMAC.EncodedLength()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + sigLength = int(sig.EdDSASigR.EncodedLength()) + sigLength += int(sig.MldsaSig.EncodedLength()) default: panic("impossible") } @@ -1240,6 +1309,13 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { err = ed25519.WriteSignature(w, sig.EdSig) case PubKeyAlgoEd448: err = ed448.WriteSignature(w, sig.EdSig) + case PubKeyAlgoHMAC, ExperimentalPubKeyAlgoHMAC: + _, err = w.Write(sig.HMAC.EncodedBytes()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(sig.EdDSASigR.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.MldsaSig.EncodedBytes()) default: panic("impossible") } @@ -1354,6 +1430,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey, config *Config) (subpack if sig.FlagAuthenticate { flags |= KeyFlagAuthenticate } + if sig.FlagForward { + flags |= KeyFlagForward + } if sig.FlagGroupKey { flags |= KeyFlagGroupKey } diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index 78ee37595..b0aeaa43e 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -82,6 +82,60 @@ ltm2aQaG } } +func TestSymmetricSignatureRead(t *testing.T) { + const serializedPacket = "c271040181080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc0ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} + + packet, err := Read(readerFromHex(serializedPacket)) + if err != nil { + t.Error(err) + } + + sig, ok := packet.(*Signature) + if !ok { + t.Errorf("Did not parse a signature packet") + } + + if sig.PubKeyAlgo != PubKeyAlgoHMAC { + t.Error("Wrong public key algorithm") + } + + if sig.Hash != crypto.SHA256 { + t.Error("Wrong public key algorithm") + } + + if !bytes.Equal(sig.HMAC.Bytes(), expectedHMAC) { + t.Errorf("Wrong HMAC value, got: %x, expected: %x\n", sig.HMAC.Bytes(), expectedHMAC) + } +} + +func TestExperimentalSymmetricSignatureRead(t *testing.T) { + const serializedPacket = "c272040165080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc400ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} + + packet, err := Read(readerFromHex(serializedPacket)) + if err != nil { + t.Error(err) + } + + sig, ok := packet.(*Signature) + if !ok { + t.Errorf("Did not parse a signature packet") + } + + if sig.PubKeyAlgo != ExperimentalPubKeyAlgoHMAC { + t.Error("Wrong public key algorithm") + } + + if sig.Hash != crypto.SHA256 { + t.Error("Wrong public key algorithm") + } + + if !bytes.Equal(sig.HMAC.Bytes(), expectedHMAC) { + t.Errorf("Wrong HMAC value, got: %x, expected: %x\n", sig.HMAC.Bytes(), expectedHMAC) + } +} + func TestSignatureReserialize(t *testing.T) { packet, _ := Read(readerFromHex(signatureDataHex)) sig := packet.(*Signature) diff --git a/openpgp/read.go b/openpgp/read.go index 5578797ed..b801493fc 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -23,6 +23,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -118,7 +121,10 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, + packet.PubKeyAlgoAEAD, packet.ExperimentalPubKeyAlgoAEAD, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/read_pqc_test.go b/openpgp/read_pqc_test.go new file mode 100644 index 000000000..fc8b57763 --- /dev/null +++ b/openpgp/read_pqc_test.go @@ -0,0 +1,122 @@ +package openpgp + +import ( + "bytes" + "encoding/hex" + "io" + "os" + "strconv" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +var pqcDraftVectors = map[string]struct { + armoredPrivateKeyPath string + armoredPublicKeyPath string + fingerprints []string + armoredMessagePaths []string +}{ + "v6_MlDsa65_MlKem768": { + "test_data/pqc/v6-mldsa-65-sample-sk.asc", + "test_data/pqc/v6-mldsa-65-sample-pk.asc", + []string{"a3e2e14b6a493ff930fb27321f125e9a6880338be9fb7da3ae065ea65793242f", "7dae8fbce23022607167af72a002e774e0ca379a2d7ae072384e1e8fde3265e4"}, + []string{"test_data/pqc/v6-mldsa-65-sample-message.asc"}, + }, + "v4_eddsa_MlKem768": { + "test_data/pqc/v4-eddsa-sample-sk.asc", + "test_data/pqc/v4-eddsa-sample-pk.asc", + []string{"342e5db2de345215cb2c944f7102ffed3b9cf12d", "e51dbfea51936988b5428fffa4f95f985ed61a51"}, + []string{"test_data/pqc/v4-eddsa-sample-message-v1.asc", "test_data/pqc/v4-eddsa-sample-message-v1.asc"}, + }, +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + // Read private key + privateKeyBytes, err := os.ReadFile(test.armoredPrivateKeyPath) + if err != nil { + t.Fatalf("Failed to read private key file: %v", err) + } + + // Read public key + publicKeyBytes, err := os.ReadFile(test.armoredPublicKeyPath) + if err != nil { + t.Fatalf("Failed to read public key file: %v", err) + } + + secretKey, err := ReadArmoredKeyRing(bytes.NewReader(privateKeyBytes)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(test.fingerprints) > 0 && len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if len(test.fingerprints) > 0 && hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if len(test.fingerprints) > 0 && hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != strings.Trim(string(publicKeyBytes), "\r\n") { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessagePaths { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgData, err := os.ReadFile(armoredMessage) + if err != nil { + t.Fatalf("Failed to read message file: %v", err) + } + msgReader, err := armor.Decode(bytes.NewReader(msgData)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) + } +} diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 23fd4aec1..0f59da492 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -29,6 +29,20 @@ func readerFromHex(s string) io.Reader { return bytes.NewBuffer(data) } +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + +func TestReadKeyRingWithExperimentalSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithExperimentalAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + func TestReadKeyRing(t *testing.T) { kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) if err != nil { @@ -770,7 +784,7 @@ func TestSymmetricAeadEaxOpenPGPJsMessage(t *testing.T) { } // Decrypt with key - var edp = p.(*packet.AEADEncrypted) + edp := p.(*packet.AEADEncrypted) rc, err := edp.Decrypt(packet.CipherFunction(0), key) if err != nil { panic(err) diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 670d60226..1329d5181 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -455,3 +455,48 @@ byVJHvLO/XErtC+GNIJeMg== =liRq -----END PGP MESSAGE----- ` + +// A key that contains a persistent AEAD subkey +const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM +34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z +zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ +kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB +AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK +HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg +dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG +6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS +ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU +AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F +Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR +HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j +dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck +iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf +PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA +AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L +7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe +AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj +089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI +=2WdX +-----END PGP PRIVATE KEY BLOCK----- +` + +// A key that contains a persistent AEAD subkey +const keyWithExperimentalAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go new file mode 100644 index 000000000..e13137e28 --- /dev/null +++ b/openpgp/symmetric/aead.go @@ -0,0 +1,79 @@ +package symmetric + +import ( + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "io" +) + +type AEADPublicKey struct { + Cipher algorithm.CipherFunction + AEADMode algorithm.AEADMode + FpSeed [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for encryption. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type AEADPrivateKey struct { + PublicKey AEADPublicKey + Key []byte +} + +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (priv *AEADPrivateKey, err error) { + priv, err = generatePrivatePartAEAD(rand, cipher) + if err != nil { + return + } + + priv.generatePublicPartAEAD(rand, cipher, aead) + return +} + +func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv = new(AEADPrivateKey) + key := make([]byte, cipher.KeySize()) + _, err = rand.Read(key) + if err != nil { + return + } + priv.Key = key + return +} + +func (priv *AEADPrivateKey) generatePublicPartAEAD(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (err error) { + priv.PublicKey.Cipher = cipher + priv.PublicKey.AEADMode = aead + + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + copy(priv.PublicKey.FpSeed[:], seed[:]) + return +} + +func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AEADMode) (nonce []byte, ciphertext []byte, err error) { + block := pub.Cipher.New(pub.Key) + aead := mode.New(block) + nonce = make([]byte, aead.NonceSize()) + rand.Read(nonce) + ciphertext = aead.Seal(nil, nonce, data, nil) + return +} + +func (priv *AEADPrivateKey) Decrypt(ivAndCiphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + nonceLength := mode.NonceLength() + iv := ivAndCiphertext[:nonceLength] + ciphertext := ivAndCiphertext[nonceLength:] + block := priv.PublicKey.Cipher.New(priv.Key) + aead := mode.New(block) + message, err = aead.Open(nil, iv, ciphertext, nil) + return +} diff --git a/openpgp/symmetric/experimental_aead.go b/openpgp/symmetric/experimental_aead.go new file mode 100644 index 000000000..6eebd7049 --- /dev/null +++ b/openpgp/symmetric/experimental_aead.go @@ -0,0 +1,75 @@ +package symmetric + +import ( + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "io" +) + +type ExperimentalAEADPublicKey struct { + Cipher algorithm.CipherFunction + BindingHash [32]byte + Key []byte +} + +type ExperimentalAEADPrivateKey struct { + PublicKey ExperimentalAEADPublicKey + HashSeed [32]byte + Key []byte +} + +func ExperimentalAEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *ExperimentalAEADPrivateKey, err error) { + priv, err = generatePrivatePartExperimentalAEAD(rand, cipher) + if err != nil { + return + } + + priv.generatePublicPartExperimentalAEAD(cipher) + return +} + +func generatePrivatePartExperimentalAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *ExperimentalAEADPrivateKey, err error) { + priv = new(ExperimentalAEADPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, cipher.KeySize()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *ExperimentalAEADPrivateKey) generatePublicPartExperimentalAEAD(cipher algorithm.CipherFunction) (err error) { + priv.PublicKey.Cipher = cipher + + bindingHash := ComputeBindingHash(priv.HashSeed) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + copy(priv.PublicKey.BindingHash[:], bindingHash) + return +} + +func (pub *ExperimentalAEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AEADMode) (nonce []byte, ciphertext []byte, err error) { + block := pub.Cipher.New(pub.Key) + aead := mode.New(block) + nonce = make([]byte, aead.NonceSize()) + rand.Read(nonce) + ciphertext = aead.Seal(nil, nonce, data, nil) + return +} + +func (priv *ExperimentalAEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + + block := priv.PublicKey.Cipher.New(priv.Key) + aead := mode.New(block) + message, err = aead.Open(nil, nonce, ciphertext, nil) + return +} diff --git a/openpgp/symmetric/experimental_hmac.go b/openpgp/symmetric/experimental_hmac.go new file mode 100644 index 000000000..ac4ab27e9 --- /dev/null +++ b/openpgp/symmetric/experimental_hmac.go @@ -0,0 +1,96 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "crypto/sha256" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +type ExperimentalHMACPublicKey struct { + Hash algorithm.Hash + BindingHash [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for verification. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type ExperimentalHMACPrivateKey struct { + PublicKey ExperimentalHMACPublicKey + HashSeed [32]byte + Key []byte +} + +func ExperimentalHMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *ExperimentalHMACPrivateKey, err error) { + priv, err = generatePrivatePartExperimentalHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartExperimentalHMAC(hash) + return +} + +func generatePrivatePartExperimentalHMAC(rand io.Reader, hash algorithm.Hash) (priv *ExperimentalHMACPrivateKey, err error) { + priv = new(ExperimentalHMACPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, hash.Size()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *ExperimentalHMACPrivateKey) generatePublicPartExperimentalHMAC(hash algorithm.Hash) (err error) { + priv.PublicKey.Hash = hash + + bindingHash := ComputeBindingHash(priv.HashSeed) + copy(priv.PublicKey.BindingHash[:], bindingHash) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + return +} + +func ComputeBindingHash(seed [32]byte) []byte { + bindingHash := sha256.New() + bindingHash.Write(seed[:]) + + return bindingHash.Sum(nil) +} + +func (priv *ExperimentalHMACPrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +func (priv *ExperimentalHMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + expectedMAC, err := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + if err != nil { + return + } + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *ExperimentalHMACPublicKey) Verify(digest []byte, signature []byte) (bool, error) { + expectedMAC, err := calculateMAC(pub.Hash, pub.Key, digest) + if err != nil { + return false, err + } + return hmac.Equal(expectedMAC, signature), nil +} diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go new file mode 100644 index 000000000..50755f8ec --- /dev/null +++ b/openpgp/symmetric/hmac.go @@ -0,0 +1,98 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +type HMACPublicKey struct { + Hash algorithm.Hash + FpSeed [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for verification. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type HMACPrivateKey struct { + PublicKey HMACPublicKey + Key []byte +} + +func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv, err = generatePrivatePartHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartHMAC(rand, hash) + return +} + +func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv = new(HMACPrivateKey) + + key := make([]byte, hash.Size()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.Key = key + return +} + +func (priv *HMACPrivateKey) generatePublicPartHMAC(rand io.Reader, hash algorithm.Hash) (err error) { + priv.PublicKey.Hash = hash + + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + copy(priv.PublicKey.FpSeed[:], seed[:]) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + return +} + +func (priv *HMACPrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +func (priv *HMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + expectedMAC, err := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + if err != nil { + return + } + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *HMACPublicKey) Verify(digest []byte, signature []byte) (bool, error) { + expectedMAC, err := calculateMAC(pub.Hash, pub.Key, digest) + if err != nil { + return false, err + } + return hmac.Equal(expectedMAC, signature), nil +} + +func calculateMAC(hash algorithm.Hash, key []byte, data []byte) ([]byte, error) { + hashFunc := hash.HashFunc() + if !hashFunc.Available() { + return nil, errors.UnsupportedError("hash function") + } + + mac := hmac.New(hashFunc.New, key) + mac.Write(data) + + return mac.Sum(nil), nil +} diff --git a/openpgp/test_data/pqc/v4-eddsa-sample-message-v1.asc b/openpgp/test_data/pqc/v4-eddsa-sample-message-v1.asc new file mode 100644 index 000000000..74fdbd146 --- /dev/null +++ b/openpgp/test_data/pqc/v4-eddsa-sample-message-v1.asc @@ -0,0 +1,34 @@ +-----BEGIN PGP MESSAGE----- + +wcPUA6T5X5he1hpRI8oKxrVQiCkB27ePKVHeA4pTYMKZA6u1l8syrP2+sEULDgvB +GmH6+0mTw07VEh6J1i1+3ymnnTqLhkv3YqdBtiC81+PL05YPCymPZaWf0ajq+4sM +dnBfLJ3BPrsJw03sVHIBh+L3qolG0CliIzGKxIPz9F5RBSvDdSIwCNg9hnfZjpMu +kcmceYISpWjJR+LeAieyYOTZ+Qhx71jYQ2svfpwW+XAw03uMpZkvqkOJmYr8uUca +i8x2j4G6EUXuu9NswSPPirCqU6OZVdpoHUZusFyRZz89V10fQr9hrnJOGw0VtPGz +SMEulSosvnvnK2BQ2ccJVNn0s/mk+fttQLpBBsKCH0UK8norIXt5ahxdj9sSwBTf +q6cPlHz1o9OnFSuewFkapA4PuLxhf4YY8ZTsC9LUZLiMf8MrMza7gbtnEbBzW3bx +y6QD6I+PneJl/8M5a7ECrlFuR2p3Kyt6MTiY+6sxJ1GOVhpNS24iO/LRxAUe8DRi +tFC46oEQFByp98SIWt3JoJKHcjQzLKTjRWfYhZDiUnBkoM6nYaAZdItcFsBDG3IV +1UstcmfcCugJyWi8V8XHVKdhWe3bWc1WrhieDCVpfSBD2NnRMGG+g90WtHcwhntf +n/mwyR/GLG+gRc16I5hPm84lS34+/txx745yDXdTx/szZAqQw0VW47CwF17A3wdw +cX1UDnUnf7/llFKqg8Zn/GXGIEreo5q/83Ib7dehm50APhtaKnQzoPbPPu21lw+8 +/3gisYxQbmrphEpU2KWWWrG5g8P3JG/D9wlHwDhhXPCdNFB7wthQbaDQ1WnN3WlV +BEtWMOqLTIovjrxHbn5judqLYQQgZPbMguzj5JXrQM7wVu4o0edv967oI6ZRBg4y +BtwlXWF3cgMvvAFfH8fXGXAtJw5Gz4/gxzan1q1JcOm6Akgp55J37LlPeXIKHiyX +W/J8qEbk0XgSSa5VduPfnP7AzrLWtyT5B9lqPszmR7euLXvb+tCuNa/G/ldceJO8 +6MKJVXYuYnk2qpuNWV1NPlCUH583LH2xgJ1YNk6U7ID4opcBMJuVM+MTA7cmLObE +Fdoa2TvJu5rpyrnqedHPNgE98P95kZJ/UtaIqJL+zzGkD5rip70DJPuQ6CkVwX1o +pTx2EKwi3c8H5QZtZUkLYeh8x/1LidZeLBdMLut1Lc7BmD0j0wU0PrwaSLsWAYmW +L16/cY9xPwIqmC8nST8rH94QoBl5eFkm3HrDYjJZrNybk0RKP5oLKG8QoUO2kXyW +9TfKNplK0YCfGTgKcTK6luDSM2cCdwlPdspwCRLPX7L7LZYaK8nEfAFD6Al8ZC45 +meBQgY4SuDlucygbFAHitrXLv4ukBDNRFxe+2dVzig5ryHZj97H5vp89aYH1W3gh +GS/k57744ziY/ACTz8cRxVKgM1oTYjDsNKvmM2J6ij7vRxuMBvM/kO7g+0jD2hPV +eWyIS1VKeT1LG7sRBd+GVQ9jHKpP/7YonXYUrOkpCdG/5YOX6Doo3VlVTRi00QmC +t8716eEJLd7ivorFYFELY5eOEUcjmNLSwEgBr2kPm1Mkn8RS8mCNT188nXgTzZk7 +jZ1rurNkkqQM53xMaLmQImS+N20GPoa2RdHW+R/veP7LugLO7gozMi/zz9+kcd0Y +wPahWnYsZ4uHg/zbgFMIH/iwQ04nv58gOLJfJafGarwotFvBIfl4Nd607lmVJTc0 +OjVhhisWL5WDIC82+DDu0yDLH/huzY4W7/ks3Hn/UpEwzq+A1/bY3MbQomew5hTI +99z/IeulfiT8/0POofN3lvMvTzeGuMMsBiMFp2nbCEHrwWCN9uaE3eAbj3E0OtAM +AIIfxypiV+bYm0IA58t7Ur3kMG1KZmcKG4DF5zrL1u5ArX/T7258Z7shYff7WtNU +Wfo= +-----END PGP MESSAGE----- diff --git a/openpgp/test_data/pqc/v4-eddsa-sample-message-v2.asc b/openpgp/test_data/pqc/v4-eddsa-sample-message-v2.asc new file mode 100644 index 000000000..12051f988 --- /dev/null +++ b/openpgp/test_data/pqc/v4-eddsa-sample-message-v2.asc @@ -0,0 +1,34 @@ +-----BEGIN PGP MESSAGE----- + +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 +-----END PGP MESSAGE----- diff --git a/openpgp/test_data/pqc/v4-eddsa-sample-pk.asc b/openpgp/test_data/pqc/v4-eddsa-sample-pk.asc new file mode 100644 index 000000000..326910cca --- /dev/null +++ b/openpgp/test_data/pqc/v4-eddsa-sample-pk.asc @@ -0,0 +1,38 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/openpgp/test_data/pqc/v4-eddsa-sample-sk.asc b/openpgp/test_data/pqc/v4-eddsa-sample-sk.asc new file mode 100644 index 000000000..28a98b028 --- /dev/null +++ b/openpgp/test_data/pqc/v4-eddsa-sample-sk.asc @@ -0,0 +1,41 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/openpgp/test_data/pqc/v6-mldsa-65-sample-message.asc b/openpgp/test_data/pqc/v6-mldsa-65-sample-message.asc new file mode 100644 index 000000000..385195bf2 --- /dev/null +++ b/openpgp/test_data/pqc/v6-mldsa-65-sample-message.asc @@ -0,0 +1,104 @@ +-----BEGIN PGP MESSAGE----- + +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 +ak/MpQwGKTcC2WrrqWhgq/WTs+8NCk5Vfa2P +-----END PGP MESSAGE----- diff --git a/openpgp/test_data/pqc/v6-mldsa-65-sample-pk.asc b/openpgp/test_data/pqc/v6-mldsa-65-sample-pk.asc new file mode 100644 index 000000000..74440a8c9 --- /dev/null +++ b/openpgp/test_data/pqc/v6-mldsa-65-sample-pk.asc @@ -0,0 +1,288 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +AAAAAAAAAAAABA8TGSIp +-----END PGP PUBLIC KEY BLOCK----- diff --git a/openpgp/test_data/pqc/v6-mldsa-65-sample-sk.asc b/openpgp/test_data/pqc/v6-mldsa-65-sample-sk.asc new file mode 100644 index 000000000..c0935a36f --- /dev/null +++ b/openpgp/test_data/pqc/v6-mldsa-65-sample-sk.asc @@ -0,0 +1,291 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go new file mode 100644 index 000000000..1306c510c --- /dev/null +++ b/openpgp/v2/forwarding.go @@ -0,0 +1,159 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + + if _, err = e.VerifyPrimaryKey(now, config); err != nil { + return nil, nil, err + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + keyProperties := selectKeyProperties(now, config, primary) + err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, keyProperties) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now, config) + // Filter expiration & revokal + if err != nil { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + forwardeeSubKeySelfSig := forwardeeSubKey.Bindings[0].Packet + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKeySelfSig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKeySelfSig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKeySelfSig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKeySelfSig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKeySelfSig.FlagForward = true + + err = forwardeeSubKeySelfSig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/v2/forwarding_test.go b/openpgp/v2/forwarding_test.go new file mode 100644 index 000000000..9a16273f4 --- /dev/null +++ b/openpgp/v2/forwarding_test.go @@ -0,0 +1,253 @@ +package v2 + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZQRXoxYJKwYBBAHaRw8BAQdAhxdzZ8ZP1M4UcauXSGbts38KhhAZxHNRcChs +9H7danMAAQC4tHykQmFpnlvhLYJDDc4MJm68mUB9qUls34GgKkqKNw6FzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CiwQTFggAPQUCZQRXowkQizX+kwlYIwMW +IQTYm4qmQoyzTnG0eZKLNf6TCVgjAwIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAMsQ +AQD9UHMIU418Z10UQrymhbjkGq/PHCytaaneaq5oycpN/QD/UiK3aA4+HxWhX/F2 +VrvEKL5a2xyd1AKKQ2DInF3xUg3HcQRlBFejEgorBgEEAZdVAQUBAQdAep7x8ncL +ShzEgKL6h9MAJbgX2z3BBgSLeAdg/rczKngX/woJjSg9O4DzqQOtAvdhYkDoOCNf +QgUAAP9OMqK0IwNmshCtktDy1/RTeyPKT8ItHDFAZ1ReKMA5CA63wngEGBYIACoF +AmUEV6MJEIs1/pMJWCMDFiEE2JuKpkKMs05xtHmSizX+kwlYIwMCG1wAAC5EAP9s +AbYBf9NGv1NxJvU0n0K++k3UIGkw9xgGJa3VFHFKvwEAx0DZpTVpCkJmiOFAOcfu +cSvjlMyQwsC/hAAzQpcqvwE= +=8LJg +-----END PGP PRIVATE KEY BLOCK-----` + +const forwardedMessage = `-----BEGIN PGP MESSAGE----- + +wV4DKsXbtIU9/JMSAQdA/6+foCjeUhS7Xto3fimUi6pfMQ/Ft3caHkK/1i767isw +NvG8xRbjQ0sAE1IZVGE1MBcVhCIbHhqp0h2J479Zmfn/iP7hfomYxrkJ/6UMnlEo +0kABKyyfO3QVAzBBNeq6hH27uqzwLgjWVrpgY7dmWPv0goSSaqHUda0lm+8JNUuF +wssOJTwrSwQrX3ezy5D/h/E6 +=okS+ +-----END PGP MESSAGE-----` + +const forwardedPlaintext = "Message for Bob" + +const forwardingKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYUEZJ7obRYJKwYBBAHaRw8BAQdA0rsiAXbk646zNSFtehSG8tXV+933gX9qdlcv +y3dsETr+CQEIRDbKlCJxPw4WjfCI1f90n4Kr4ymuStB7MLm/mh+IyheqJgLtD4ak +EhgPd3R4o9TjQnwNbHnIfPo+FBbuo9T8yfnGzz0RvpL/ReZOViVdzRtjaGFybGll +IDxjaGFybGllQHByb3Rvbi5tZT7CjwQTFggAQQUCZJ7obQmQr3ZWGFoRxXwWIQTQ +TSCJvfPq/1Z83TKvdlYYWhHFfAIbAwIeAQIZAQMLCQcCFQgDFgACBScJAgcCAACM +OgD/cEsqqZdYl/RvYG3Kew658THsRFSGKeoEOZMvC0Ubza8BAIk6/dJNIYVvEBne +gCHO0yCfIITw5pH4SoF3okqOdaIKx54EZJ7obRIKKwYBBAGXVQEFAQEHQPNm6WCv +WZOZVKx0pYZJPWDxA1BfUrHStlBiaPqWHPkmF/8KCQ2qVg8YlFj8Z6f13kH8i+iY +FuX1/gkBCEQ2ypQicT8Oyr4aomc4TdKzvSb+xZA6xYugIUFzV4ojuS9UAuOB6yd2 +Ye66Exx6qz3kpxcDgbcf3ZRO/ljZT8XWItM7j/wiUrjxuxHw4cJ4BBgWCAAqBQJk +nuhtCZCvdlYYWhHFfBYhBNBNIIm98+r/VnzdMq92VhhaEcV8AhtQAADBagD+IrnW +ecLlUsQEhs4brBFXTpF5jy0p/aAjJ9AkNoYvS9YA/27VaHCJzZwJsc7HQWOxQB+V +gZt8hzaHXTuA3JwjuKEB +=DPb7 +-----END PGP PRIVATE KEY BLOCK-----` + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) + if err != nil { + t.Error(err) + return + } + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) + if err != nil { + t.Error(err) + return + } + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) + if err != nil { + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingFull(t *testing.T) { + keyConfig := &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + Curve: packet.Curve25519, + } + + plaintext := make([]byte, 1024) + rand.Read(plaintext) + + bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig) + if err != nil { + t.Fatal(err) + } + + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) + } + + if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + _, err = w.Write(plaintext) + if err != nil { + t.Fatal(err) + } + + err = w.Close() + if err != nil { + t.Fatal(err) + } + + encrypted := buf.Bytes() + + // Decrypt message for Bob + m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("decrypted does not match original") + } + + // Forward message + transformed := transformTestMessage(t, encrypted, instances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } + + // Setup further forwarding + danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingKeyNotEncrypt(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardingKey)) + if err != nil { + t.Error(err) + return + } + if _, ok := charlesKey[0].EncryptionKey(time.Time{}, nil); ok { + t.Fatal("Marked forwarding keys should not be usable for encryption") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { + bytesReader := bytes.NewReader(encrypted) + packets := packet.NewReader(bytesReader) + splitPoint := int64(0) + transformedEncryptedKey := bytes.NewBuffer(nil) + +Loop: + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error in parsing message: %s", err) + } + switch p := p.(type) { + case *packet.EncryptedKey: + tp, err := p.ProxyTransform(instance) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = tp.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + return transformed +} + +func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity { + serializedEntity := bytes.NewBuffer(nil) + err := key.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatalf("Error in serializing forwardee key: %s", err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatalf("Error in reading forwardee key: %s", err) + } + + if len(el) != 1 { + t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el)) + } + + return el[0] +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index f0767cf91..dfe1087de 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,7 +21,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "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/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -399,6 +402,27 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.PubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.HMACGenerateKey(config.Random(), hash) + case packet.ExperimentalPubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.ExperimentalHMACGenerateKey(config.Random(), hash) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -406,6 +430,7 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // newDecrypter generates an encryption/decryption key. func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { + pubKeyAlgo := config.PublicKeyAlgorithm() switch config.PublicKeyAlgorithm() { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() @@ -441,6 +466,33 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) + case packet.PubKeyAlgoHMAC, packet.PubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + aead := algorithm.AEADMode(config.AEAD().Mode()) + return symmetric.AEADGenerateKey(config.Random(), cipher, aead) + case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + return symmetric.ExperimentalAEADGenerateKey(config.Random(), cipher) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() && pubKeyAlgo == packet.PubKeyAlgoMlkem1024X448 { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem1024_x448 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index 2ff1202b3..77a431367 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -126,6 +126,7 @@ func (e *Entity) EncryptionKeyWithError(now time.Time, config *packet.Config) (K // Iterate the keys to find the newest, unexpired one var latestSelectionError *errors.ErrEncryptionKeySelection candidateSubkey := -1 + isPQ := false var maxTime time.Time var selectedSubkeySelfSig *packet.Signature for i, subkey := range e.Subkeys { @@ -153,10 +154,11 @@ func (e *Entity) EncryptionKeyWithError(now time.Time, config *packet.Config) (K latestSelectionError = subkeyErr(err) continue } - if maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix() { + if maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix() || (!isPQ && subkey.IsPQ()) { candidateSubkey = i selectedSubkeySelfSig = subkeySelfSig maxTime = subkeySelfSig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -206,12 +208,12 @@ func (e *Entity) DecryptionKeys(id uint64, date time.Time, config *packet.Config for _, subkey := range e.Subkeys { subkeySelfSig, err := subkey.LatestValidBindingSignature(date, config) if err == nil && - (config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo, config)) && + (config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo, config)) && (id == 0 || subkey.PublicKey.KeyId == id) { keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, subkeySelfSig}) } } - if config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo, config) { + if config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo, config) { keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) } return @@ -255,6 +257,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config } // Iterate the keys to find the newest, unexpired one. + isPQ := false candidateSubkey := -1 var maxTime time.Time var selectedSubkeySelfSig *packet.Signature @@ -265,10 +268,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config (flags&packet.KeyFlagSign == 0 || isValidSigningKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo, config)) && checkKeyRequirements(subkey.PublicKey, config) == nil && (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkeySelfSig.CreationTime selectedSubkeySelfSig = subkeySelfSig + isPQ = subkey.IsPQ() } } @@ -651,6 +656,12 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo // Serialize writes the public part of the given Entity to w, including // signatures from other entities. No private key material will be output. func (e *Entity) Serialize(w io.Writer) error { + if e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoAEAD || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + return errors.InvalidArgumentError("Can't serialize symmetric primary key") + } if err := e.PrimaryKey.Serialize(w); err != nil { return err } @@ -671,6 +682,18 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // The types of keys below are only useful as private keys. Thus, the + // public key packets contain no meaningful information and do not need + // to be serialized. + // Prevent public key export for forwarding keys, see forwarding section 4.1. + subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{}, nil) + if subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoAEAD || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + (err == nil && subKeySelfSig.FlagForward) { + continue + } if err := subkey.Serialize(w, false); err != nil { return err } @@ -847,3 +870,15 @@ func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgo return config.AllowAllKeyFlagsWhenMissing() } + +func isValidDecryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm, config *packet.Config) bool { + if !algo.CanEncrypt() { + return false + } + + if signature.FlagsValid { + return signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage + } + + return config.AllowAllKeyFlagsWhenMissing() +} diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index 47f2d4930..ab41b6606 100644 --- a/openpgp/v2/keys_test.go +++ b/openpgp/v2/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -2109,3 +2110,440 @@ func TestEncryptionKeyError(t *testing.T) { t.Fatal("wrong error") } } + +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.PubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 128 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 129 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.PubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { + t.Error("parsed wrong aead mode") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed + i = bytes.Index(w.Bytes(), firstFpSeed[:]) + + secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed + k = bytes.Index(w.Bytes(), secondFpSeed[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + +func TestSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 +NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT +eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF +BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu +b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l +BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA +QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs +AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ +yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb +3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p +Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT +=ywfD +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.PubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.PubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} + +func TestExperimentalAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestExperimentalSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestExperimentalAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Fatalf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Fatal("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoExperimentalSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.ExperimentalAEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.ExperimentalHMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.ExperimentalAEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.ExperimentalHMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + +func TestExperimentalSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index 7980c9b2a..0c3643a3d 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -26,6 +26,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -136,9 +139,10 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, - packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, + packet.PubKeyAlgoAEAD, packet.ExperimentalPubKeyAlgoAEAD, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/v2/read_pqc_test.go b/openpgp/v2/read_pqc_test.go new file mode 100644 index 000000000..bba8c6aef --- /dev/null +++ b/openpgp/v2/read_pqc_test.go @@ -0,0 +1,123 @@ +package v2 + +import ( + "bytes" + "encoding/hex" + "io" + "os" + "strconv" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +var pqcDraftVectors = map[string]struct { + armoredPrivateKeyPath string + armoredPublicKeyPath string + fingerprints []string + armoredMessagePaths []string +}{ + + "v6_MlDsa65_MlKem768": { + "../test_data/pqc/v6-mldsa-65-sample-sk.asc", + "../test_data/pqc/v6-mldsa-65-sample-pk.asc", + []string{"a3e2e14b6a493ff930fb27321f125e9a6880338be9fb7da3ae065ea65793242f", "7dae8fbce23022607167af72a002e774e0ca379a2d7ae072384e1e8fde3265e4"}, + []string{"../test_data/pqc/v6-mldsa-65-sample-message.asc"}, + }, + "v4_eddsa_MlKem768": { + "../test_data/pqc/v4-eddsa-sample-sk.asc", + "../test_data/pqc/v4-eddsa-sample-pk.asc", + []string{"342e5db2de345215cb2c944f7102ffed3b9cf12d", "e51dbfea51936988b5428fffa4f95f985ed61a51"}, + []string{"../test_data/pqc/v4-eddsa-sample-message-v1.asc", "../test_data/pqc/v4-eddsa-sample-message-v1.asc"}, + }, +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + // Read private key + privateKeyBytes, err := os.ReadFile(test.armoredPrivateKeyPath) + if err != nil { + t.Fatalf("Failed to read private key file: %v", err) + } + + // Read public key + publicKeyBytes, err := os.ReadFile(test.armoredPublicKeyPath) + if err != nil { + t.Fatalf("Failed to read public key file: %v", err) + } + + secretKey, err := ReadArmoredKeyRing(bytes.NewReader(privateKeyBytes)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != strings.Trim(string(publicKeyBytes), "\r\n") { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessagePaths { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgData, err := os.ReadFile(armoredMessage) + if err != nil { + t.Fatalf("Failed to read message file: %v", err) + } + msgReader, err := armor.Decode(bytes.NewReader(msgData)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) + } +} diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index f167c34e2..504dea54d 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -30,6 +30,20 @@ func readerFromHex(s string) io.Reader { return bytes.NewBuffer(data) } +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + +func TestReadKeyRingWithExperimentalSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithExperimentalAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + func TestReadKeyRing(t *testing.T) { kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) if err != nil { diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 802387030..689cf5f2b 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -842,3 +842,48 @@ Z9JTerD46t0XuA6/v+W0j4uG5frNxfZ2D2AoNBW0yu6wgo5MM4IA+PH0xhtj0xa3 e9Jwn5aNHhCQpFB3y/FDXAEAAP//nNYTdw== =ETAD -----END PGP MESSAGE-----` + +// A key that contains a persistent AEAD subkey +const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM +34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z +zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ +kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB +AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK +HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg +dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG +6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS +ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU +AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F +Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR +HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j +dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck +iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf +PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA +AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L +7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe +AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj +089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI +=2WdX +-----END PGP PRIVATE KEY BLOCK----- +` + +// A key that contains a persistent AEAD subkey +const keyWithExperimentalAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go index f056ae7d4..d50c74845 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -208,3 +208,15 @@ func (s *Subkey) LatestValidBindingSignature(date time.Time, config *packet.Conf } return selectedSig, nil } + +// IsPQ returns true if the algorithm is Post-Quantum safe +func (s *Subkey) IsPQ() bool { + switch s.PublicKey.PubKeyAlgo { + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } + +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 0692cec6c..d1147812f 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -594,8 +594,8 @@ func encrypt( encryptKeys := make([]Key, len(to)+len(toHidden)) config := params.Config - // AEAD is used if every key supports it - aeadSupported := true + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil var intendedRecipients []*packet.Recipient // Intended Recipient Fingerprint subpacket SHOULD be used when creating a signed and encrypted message @@ -610,11 +610,17 @@ func encrypt( // Override the time to select the encryption key with the provided one. timeForEncryptionKey = *params.EncryptionTime } + + allPQ := len(encryptKeys) > 0 for i, recipient := range append(to, toHidden...) { if encryptKeys[i], err = recipient.EncryptionKeyWithError(timeForEncryptionKey, config); err != nil { return nil, err } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := recipient.PrimarySelfSignature(timeForEncryptionKey, config) if primarySelfSignature == nil { return nil, errors.StructuralError("entity without a self-signature") @@ -641,8 +647,12 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -1021,7 +1031,7 @@ func parseOutsideSig(outsideSig []byte) (outSigPacket *packet.Signature, err err func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { switch singingKey.PubKeyAlgo { - case packet.PubKeyAlgoEd448: + case packet.PubKeyAlgoEd448, packet.PubKeyAlgoMldsa87Ed448: return []uint8{ hashToHashId(crypto.SHA512), hashToHashId(crypto.SHA3_512), diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index f3c4f9da7..69609b3d4 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -6,6 +6,7 @@ package v2 import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -434,33 +435,6 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { - keyRingHex string - isSigned bool - okV6 bool -}{ - { - testKeys1And2PrivateHex, - false, - true, - }, - { - testKeys1And2PrivateHex, - true, - true, - }, - { - dsaElGamalTestKeysHex, - false, - false, - }, - { - dsaElGamalTestKeysHex, - true, - false, - }, -} - func TestIntendedRecipientsEncryption(t *testing.T) { var config = &packet.Config{ V6Keys: true, @@ -674,129 +648,176 @@ func TestMultiSignEncryption(t *testing.T) { } } -func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) +var testEncryptionTests = map[string]struct { + keyRingHex string + isSigned bool + okV6 bool +}{ + "Simple": { + testKeys1And2PrivateHex, + false, + true, + }, + "Simple_signed": { + testKeys1And2PrivateHex, + true, + true, + }, + "DSA_ElGamal": { + dsaElGamalTestKeysHex, + false, + false, + }, + "DSA_ElGamal_signed": { + dsaElGamalTestKeysHex, + true, + false, + }, + // TODO: Add test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ +} - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) +func TestEncryption(t *testing.T) { + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - config := allowAllAlgorithmsConfig - config.DefaultCompressionAlgo = compAlgo - config.CompressionConfig = compConf - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed []*Entity + if test.isSigned { + signed = kring[:1] } - config.AEADConfig = &aeadConf - } - var signers []*Entity - if signed != nil { - signers = []*Entity{signed} - } - w, err := Encrypt(buf, kring[:1], nil, signers, nil /* no hints */, &config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } - - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = compAlgo + config.CompressionConfig = compConf + config.DefaultCipher = packet.CipherAES256 + + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], nil, signed, nil /* no hints */, &config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + return + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := signKey.PublicKey.KeyId - if len(md.SignatureCandidates) < 1 { - t.Error("no candidate signature found") + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) } - if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignatureCandidates[0].SignedByEntity == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") - encryptKey, _ := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("no candidate signature found") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%s: message signed by wrong key id, got: %v, want: %v", name, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%s: failed to find the signing Entity", name) + } + } - if test.isSigned { - if !md.IsVerified { - t.Errorf("not verified despite all data read") + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) } - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + + encryptKey, out := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + if !out { + t.Fatalf("#%s: No encryption key found", name) } - if md.Signature == nil { - t.Error("signature missing") + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - } + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) + } + + if test.isSigned { + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -997,3 +1018,90 @@ FindKey: } return nil } + +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, []*Entity{entity}, msg, c) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, _, err = VerifyDetachedSignature(entityList, msg, sig, c) + if err != nil { + t.Fatal(err) + } +} diff --git a/openpgp/write.go b/openpgp/write.go index 84bc27d83..822af8f9d 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -369,6 +369,7 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // AEAD is used only if config enables it and every key supports it aeadSupported := config.AEAD() != nil + allPQ := len(to) > 0 for i := range to { var ok bool encryptKeys[i], ok = to[i].EncryptionKey(config.Now()) @@ -376,6 +377,10 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := to[i].PrimarySelfSignature() if primarySelfSignature == nil { return nil, errors.InvalidArgumentError("entity without a self-signature") @@ -402,8 +407,12 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -656,7 +665,7 @@ func selectHash(candidateHashes []byte, configuredHash crypto.Hash, signer *pack func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { switch singingKey.PubKeyAlgo { - case packet.PubKeyAlgoEd448: + case packet.PubKeyAlgoEd448, packet.PubKeyAlgoMldsa87Ed448: return []uint8{ hashToHashId(crypto.SHA512), hashToHashId(crypto.SHA3_512), diff --git a/openpgp/write_test.go b/openpgp/write_test.go index c928236b0..bcda90846 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -6,6 +6,7 @@ package openpgp import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -263,6 +264,91 @@ func TestNewEntity(t *testing.T) { } } +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, entity, msg, nil) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, err = CheckDetachedSignature(entityList, msg, sig, nil) + if err != nil { + t.Fatal(err) + } +} + func TestEncryptWithCompression(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) passphrase := []byte("passphrase") @@ -436,148 +522,162 @@ func TestSymmetricEncryptionSEIPDv2RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { +var testEncryptionTests = map[string]struct { keyRingHex string isSigned bool okV6 bool }{ - { + "Simple": { testKeys1And2PrivateHex, false, true, }, - { + "Simple_signed": { testKeys1And2PrivateHex, true, true, }, - { + "DSA_ElGamal": { dsaElGamalTestKeysHex, false, false, }, - { + "DSA_ElGamal_signed": { dsaElGamalTestKeysHex, true, false, }, + //TODO: Update test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ } func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) - - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - var config = &packet.Config{ - DefaultCompressionAlgo: compAlgo, - CompressionConfig: compConf, - } - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed *Entity + if test.isSigned { + signed = kring[0] } - config.AEADConfig = &aeadConf - } - w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + var config = &packet.Config{ + DefaultCompressionAlgo: compAlgo, + CompressionConfig: compConf, + DefaultCipher: packet.CipherAES256, + } - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime) - expectedKeyId := signKey.PublicKey.KeyId - if md.SignedByKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId) + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignedBy == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - encryptKey, _ := kring[0].EncryptionKey(testTime) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime) + expectedKeyId := signKey.PublicKey.KeyId + if md.SignedByKeyId != expectedKeyId { + t.Errorf("Message signed by wrong key id, got: %v, want: %v", *md.SignedBy, expectedKeyId) + } + if md.SignedBy == nil { + t.Error("#Failed to find the signing Entity") + } + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } - if test.isSigned { - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + encryptKey, _ := kring[0].EncryptionKey(testTime) + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - if md.Signature == nil { - t.Error("signature missing") + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) } - } + + if test.isSigned { + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -698,7 +798,8 @@ ParsePackets: case *packet.EncryptedKey: // This packet contains the decryption key encrypted to a public key. switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue