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/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/end_to_end_test.go b/openpgp/integration_tests/v2/end_to_end_test.go index d44792375..ca915c10e 100644 --- a/openpgp/integration_tests/v2/end_to_end_test.go +++ b/openpgp/integration_tests/v2/end_to_end_test.go @@ -372,7 +372,7 @@ func readArmoredPk(t *testing.T, publicKey string) openpgp.EntityList { if len(keys) < 1 { t.Errorf("Failed to read key with good cross signature, %d", len(keys)) } - if len(keys[0].Subkeys) < 1 { + if len(keys[0].Subkeys) < 1 && keys[0].PrimaryKey.PubKeyAlgo != packet.PubKeyAlgoAEAD { t.Errorf("Failed to read good subkey, %d", len(keys[0].Subkeys)) } return keys @@ -386,7 +386,7 @@ func readArmoredSk(t *testing.T, sk string, pass string) openpgp.EntityList { if len(keys) != 1 { t.Errorf("Failed to read key with good cross signature, %d", len(keys)) } - if len(keys[0].Subkeys) < 1 { + if len(keys[0].Subkeys) < 1 && keys[0].PrimaryKey.PubKeyAlgo != packet.PubKeyAlgoAEAD { t.Errorf("Failed to read good subkey, %d", len(keys[0].Subkeys)) } keyObject := keys[0].PrivateKey diff --git a/openpgp/integration_tests/v2/testdata/test_vectors.json b/openpgp/integration_tests/v2/testdata/test_vectors.json index d5fc0d25a..cf300ef69 100644 --- a/openpgp/integration_tests/v2/testdata/test_vectors.json +++ b/openpgp/integration_tests/v2/testdata/test_vectors.json @@ -94,5 +94,13 @@ "password": "", "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlNgEWrqKjRMJKyQDAwIIAQENBAMEDI4HTYTe2L0kzVVIJUrN7+8KivNLQNRUeLFp\noeKHeEqZdzv2zuYsqW91wTcxuobHuyhz2Rw/6GuxCjHMZKe1OEKzO73MHvk5x3Ut\ngsU3ziA4GYM1fPJobOJXwEvB7Hdt1jPnX6FDzsnnDb/V08o7oRDiFF+nakbZ4fdh\nBGPtoO8AAf94HXntn7GFtPoPKEG4GjDchW7kLoi99khL4afe/rEuuWDT4ue/osUw\ngDxddaBV2HpYlJtljbALvS+Fqlaz6+7mJOG0InRlc3Q1MTJAZ29jcnlwdG8gPHRl\nc3Q1MTJAZ29jcnlwdD6I0AQTEwoAOBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJa\nuoqNAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEF/7WVyCi79OH2YCAIdC\nLCIm4/8dTT3RROYFK/ERghCXH6Q42U5ojGdVc4+HSLiWyUijjqSznhrRtY4Izfcd\n23eLZCo1TjvfPWsK8N0B/1eRbHersapjqA2jhHjbhGr6pyZhmBZtMpeqJ80R86Ka\ncFcRH008VrqJoZwSRt+BQHDdwiWG+ZQiTY8Y9KmZ5pSc3ARauoqNEgkrJAMDAggB\nAQ0EAwQ0T5L5YcJMUFF8OYFvHlwcym3o3APGsbUCvfhUguYWOyEr9drNiYCaC0gl\nbO+KackLO30gWiVg1gHY3CoJfi0kRwt1tXKkiqh7wCD2BPDzP0lhF3nHaAKNpg3h\nmpGUcZui9FyTezwK6CjqBgAhLETCwevB1MXQuvvPgw9uil7rbQMBCgkAAf96G+z/\nK9vrz/nhvZessIeGAEHhmrGkHLBpw+9IGDs7hWNvisTn99JezTtPbYMInrwmEOB9\n9VoAIO8Xrom5XFXjI42IuAQYEwoAIBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJa\nuoqNAhsMAAoJEF/7WVyCi79O3QIB/1e/HKOHV7504x0wu14qXDN+2QW8P6j8d0qI\nGB7xOegf8Z8KgzGywZFjTT6GKBqPTz2vMd4u44/sLBVBgPgiKQgB/308ETfQaPcz\nctjlrmykdX0TrdiKLy92xAqsohFff5Ri5pr500005rTYJfNYN+Cug6u9UygWL2RY\nu4H95mtsxZo=\n=Qb7k\n-----END PGP PRIVATE KEY BLOCK-----", "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmJMEWrqKjRMJKyQDAwIIAQENBAMEDI4HTYTe2L0kzVVIJUrN7+8KivNLQNRUeLFp\noeKHeEqZdzv2zuYsqW91wTcxuobHuyhz2Rw/6GuxCjHMZKe1OEKzO73MHvk5x3Ut\ngsU3ziA4GYM1fPJobOJXwEvB7Hdt1jPnX6FDzsnnDb/V08o7oRDiFF+nakbZ4fdh\nBGPtoO+0InRlc3Q1MTJAZ29jcnlwdG8gPHRlc3Q1MTJAZ29jcnlwdD6I0AQTEwoA\nOBYhBFZP0ImPk6gNK4o9sl/7WVyCi79OBQJauoqNAhsDBQsJCAcCBhUICQoLAgQW\nAgMBAh4BAheAAAoJEF/7WVyCi79OH2YCAIdCLCIm4/8dTT3RROYFK/ERghCXH6Q4\n2U5ojGdVc4+HSLiWyUijjqSznhrRtY4Izfcd23eLZCo1TjvfPWsK8N0B/1eRbHer\nsapjqA2jhHjbhGr6pyZhmBZtMpeqJ80R86KacFcRH008VrqJoZwSRt+BQHDdwiWG\n+ZQiTY8Y9KmZ5pS4lwRauoqNEgkrJAMDAggBAQ0EAwQ0T5L5YcJMUFF8OYFvHlwc\nym3o3APGsbUCvfhUguYWOyEr9drNiYCaC0glbO+KackLO30gWiVg1gHY3CoJfi0k\nRwt1tXKkiqh7wCD2BPDzP0lhF3nHaAKNpg3hmpGUcZui9FyTezwK6CjqBgAhLETC\nwevB1MXQuvvPgw9uil7rbQMBCgmIuAQYEwoAIBYhBFZP0ImPk6gNK4o9sl/7WVyC\ni79OBQJauoqNAhsMAAoJEF/7WVyCi79O3QIB/1e/HKOHV7504x0wu14qXDN+2QW8\nP6j8d0qIGB7xOegf8Z8KgzGywZFjTT6GKBqPTz2vMd4u44/sLBVBgPgiKQgB/308\nETfQaPczctjlrmykdX0TrdiKLy92xAqsohFff5Ri5pr500005rTYJfNYN+Cug6u9\nUygWL2RYu4H95mtsxZo=\n=3o0a\n-----END PGP PUBLIC KEY BLOCK-----" + }, + { + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwVwDumfyu2+69IYAA510KQYJEHWRQTL2CL0/gH4PNaq76jYVOoNeRQ8JbXN9\nB3/s0+Y52K47kPUTMhW+kRj4NKlYsK/WhFOUoZpdBzf5qp8ybaD+pTfdUJ8K\niPlgCdLAYQHGQeg3GxvQBxX6ElxWR3SJPm0UsDGoEmVQ9MXBUmAqYi3t/3V2\nQHWOzPiyMy5gfqtv9ug0q1qbY8WgqNYhGcIsivTHD94XqYUKe5EnwPIKBN0L\nADbmDvObvZumBYb7mzyp6GeOhzpe1yYW8QnDxDKg3WK4k4TGlQJb8LQyP/yP\nKegea0hpyYu6Om/dQH30MzPIwvbweUGy010eF3lBL6DvSSrdJbZF4fBlAT75\nzlZkEmQvUiIInDvATDVoDbGjq6fQS/tDWHOz5xxdzQJ57K/fGa/mGiVHFesq\nFfWRK0lmySbHUqEWzNpBdEKlUuvso3grWLEVY+1YKUsHdTcXu0+f3ESt1kTe\nMfZINYfC4gGY5fdThqNJUzv34yXX9TOmyO0=\n=AOzJ\n-----END PGP MESSAGE-----", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", + "name": "aead", + "password": "", + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n6EwGaXT0TgAAAAAhCWXcoYn5e9mbCXz0RJmbldb+3noZXSWT0+OhS8RsPUMk\nAATt3aRmUmeTbwf1UiciJ+9elS4sRdHRY7HHrGtUcIq0\n-----END PGP PRIVATE KEY BLOCK-----\n", + "publicKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n6EwGaXT0TgAAAAAhCWXcoYn5e9mbCXz0RJmbldb+3noZXSWT0+OhS8RsPUMk\nAATt3aRmUmeTbwf1UiciJ+9elS4sRdHRY7HHrGtUcIq0\n-----END PGP PRIVATE KEY BLOCK-----\n" } ] 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/keys.go b/openpgp/keys.go index d9d3927ec..9e677e958 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -384,7 +384,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}) } } @@ -803,6 +803,11 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // Don't export public key for forwarding keys, see forwarding section 4.1. + if subkey.Sig.FlagForward { + continue + } + err = subkey.PublicKey.Serialize(w) if err != nil { return err 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 c544d0f0b..4b9c960f8 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -21,6 +21,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" + "golang.org/x/crypto/hkdf" ) // EncryptedKey represents a public-key encrypted session key. See RFC 4880, @@ -38,7 +39,9 @@ type EncryptedKey struct { encryptedMPI2 encoding.Field // used for Elgamal and ECDH ephemeralPublicEcc []byte // used for X25519, X448 and ML-KEM ephemeralPublicMlKem []byte // used for ML-KEM - encryptedSession []byte // used for X25519, X448 and ML-KEM + encryptedSession []byte // used for X25519, X448, ML-KEM and AEAD + aeadSalt []byte // used for AEAD + aeadMode AEADMode // used for AEAD } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -100,6 +103,24 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { e.Algo = PublicKeyAlgorithm(buf[0]) var cipherFunction byte switch e.Algo { + case PubKeyAlgoAEAD: + _, err = readFull(r, buf[:1]) + if err != nil { + return + } + e.aeadMode = AEADMode(buf[0]) + if !e.aeadMode.IsSupported() { + return errors.UnsupportedError("unsupported AEAD mode in PKESK") + } + e.aeadSalt = make([]byte, 32) + _, err = readFull(r, e.aeadSalt) + if err != nil { + return + } + e.encryptedSession, err = io.ReadAll(r) + if err != nil { + return + } case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: e.encryptedMPI1 = new(encoding.MPI) if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { @@ -179,6 +200,27 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { // TODO(agl): use session key decryption routines here to avoid // padding oracle attacks. switch priv.PubKeyAlgo { + case PubKeyAlgoAEAD: + pk := priv.PublicKey.PublicKey.(*PersistentSymmetricKeyPublicFields) + sk := priv.PrivateKey.(*PersistentSymmetricKeyPrivateFields) + packetID := 0xC0 | packetTypeEncryptedKey + version := e.Version + info := []byte{byte(packetID), byte(version), byte(pk.SymmetricAlgorithm), byte(e.aeadMode)} + hkdf := hkdf.New(crypto.SHA512.New, sk.Key, e.aeadSalt, info) + keySize := pk.SymmetricAlgorithm.KeySize() + ivLength := e.aeadMode.IvLength() + encKey := make([]byte, keySize) + iv := make([]byte, ivLength) + _, err = hkdf.Read(encKey) + if err != nil { + return err + } + _, err = hkdf.Read(iv) + if err != nil { + return err + } + modeInstance := e.aeadMode.new(pk.SymmetricAlgorithm.new(encKey)) + b, err = modeInstance.Open(b, iv, e.encryptedSession, []byte{}) case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: // Supports both *rsa.PrivateKey and crypto.Decrypter k := priv.PrivateKey.(crypto.Decrypter) @@ -212,6 +254,16 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { var key []byte switch priv.PubKeyAlgo { + case PubKeyAlgoAEAD: + keyOffset := 0 + if e.Version < 6 { + e.CipherFunc = CipherFunction(b[0]) + keyOffset = 1 + if !e.CipherFunc.IsSupported() { + return errors.UnsupportedError("unsupported encryption function") + } + } + key = b[keyOffset:] case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: keyOffset := 0 if e.Version < 6 { @@ -246,6 +298,8 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { func (e *EncryptedKey) Serialize(w io.Writer) error { var encodedLength int switch e.Algo { + case PubKeyAlgoAEAD: + encodedLength = 1 /* AEAD mode */ + len(e.aeadSalt) + len(e.encryptedSession) case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: encodedLength = int(e.encryptedMPI1.EncodedLength()) case PubKeyAlgoElGamal: @@ -310,6 +364,15 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { } switch e.Algo { + case PubKeyAlgoAEAD: + if _, err := w.Write([]byte{byte(e.aeadMode)}); err != nil { + return err + } + if _, err := w.Write(e.aeadSalt); err != nil { + return err + } + _, err := w.Write(e.encryptedSession) + return err case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly: _, err := w.Write(e.encryptedMPI1.EncodedBytes()) return err @@ -464,6 +527,130 @@ 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 +} + +// SerializeEncryptedKeyPSK serializes an encrypted key packet to w that contains +// key, encrypted with a persistent symmetric key. +// Offers the hidden flag option to indicated if the PKESK packet should include a wildcard KeyID. +// If aeadSupported is set, PKESK v6 is used, otherwise v3. +// Note: aeadSupported MUST match the value passed to SerializeSymmetricallyEncrypted. +// If config is nil, sensible defaults will be used. +func SerializeEncryptedKeyPSK(w io.Writer, psk *PersistentSymmetricKey, cipherFunc CipherFunction, aeadSupported bool, key []byte, config *Config) error { + var buf [36]byte // max possible header size is v6 + lenHeaderWritten := versionSize + version := 3 + + if aeadSupported { + version = 6 + } + + buf[0] = byte(version) + + if version == 6 { + // A one-octet size of the following two fields. + buf[1] = byte(keyVersionSize + len(psk.Fingerprint)) + // A one octet key version number. + buf[2] = byte(psk.Version) + lenHeaderWritten += keyVersionSize + 1 + // The fingerprint of the public key + copy(buf[lenHeaderWritten:lenHeaderWritten+len(psk.Fingerprint)], psk.Fingerprint) + lenHeaderWritten += len(psk.Fingerprint) + } else { + binary.BigEndian.PutUint64(buf[versionSize:(versionSize+keyIdSize)], psk.KeyId) + lenHeaderWritten += keyIdSize + } + buf[lenHeaderWritten] = byte(psk.PubKeyAlgo) + lenHeaderWritten += algorithmSize + + lenKeyBlock := len(key) + if version < 6 { + lenKeyBlock += 1 // cipher type included + } + keyBlock := make([]byte, lenKeyBlock) + keyOffset := 0 + if version < 6 { + keyBlock[0] = byte(cipherFunc) + keyOffset = 1 + } + copy(keyBlock[keyOffset:], key) + + pk := psk.PublicKey.PublicKey.(*PersistentSymmetricKeyPublicFields) + sk := psk.PrivateKey.PrivateKey.(*PersistentSymmetricKeyPrivateFields) + packetID := 0xC0 | packetTypeEncryptedKey + aeadMode := config.AEAD().Mode() + info := []byte{byte(packetID), byte(version), byte(pk.SymmetricAlgorithm), byte(aeadMode)} + salt := make([]byte, 32) + _, err := io.ReadFull(config.Random(), salt) + if err != nil { + return err + } + hkdf := hkdf.New(crypto.SHA512.New, sk.Key, salt, info) + keySize := pk.SymmetricAlgorithm.KeySize() + ivLength := aeadMode.IvLength() + encKey := make([]byte, keySize) + iv := make([]byte, ivLength) + _, err = hkdf.Read(encKey) + if err != nil { + return err + } + _, err = hkdf.Read(iv) + if err != nil { + return err + } + modeInstance := aeadMode.new(pk.SymmetricAlgorithm.new(encKey)) + var ciphertext []byte + ciphertext = modeInstance.Seal(ciphertext, iv, keyBlock, []byte{}) + + packetLen := lenHeaderWritten /* header length */ + 1 /* AEAD mode */ + len(salt) + len(ciphertext) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + _, err = w.Write(buf[:lenHeaderWritten]) + if err != nil { + return err + } + _, err = w.Write([]byte{byte(aeadMode)}) + if err != nil { + return err + } + _, err = w.Write(salt) + if err != nil { + return err + } + _, err = w.Write(ciphertext) + return err +} + 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 { 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 6f84a25ed..15ae27954 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -319,7 +319,8 @@ const ( packetTypeUserAttribute packetType = 17 packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18 packetTypeAEADEncrypted packetType = 20 - packetPadding packetType = 21 + packetTypePadding packetType = 21 + packetTypePersistentSymmetricKey packetType = 40 ) // EncryptedDataPacket holds encrypted data. It is currently implemented by @@ -370,8 +371,10 @@ func Read(r io.Reader) (p Packet, err error) { p = se case packetTypeAEADEncrypted: p = new(AEADEncrypted) - case packetPadding: + case packetTypePadding: p = Padding(len) + case packetTypePersistentSymmetricKey: + p = new(PersistentSymmetricKey) case packetTypeMarker: p = new(Marker) case packetTypeTrust: @@ -434,7 +437,7 @@ func ReadWithCheck(r io.Reader, sequence *SequenceVerifier) (p Packet, msgErr er case packetTypeAEADEncrypted: msgErr = sequence.Next(EncSymbol) p = new(AEADEncrypted) - case packetPadding: + case packetTypePadding: p = Padding(len) case packetTypeMarker: p = new(Marker) @@ -492,6 +495,7 @@ const ( type PublicKeyAlgorithm uint8 const ( + PubKeyAlgoAEAD PublicKeyAlgorithm = 0 PubKeyAlgoRSA PublicKeyAlgorithm = 1 PubKeyAlgoElGamal PublicKeyAlgorithm = 16 PubKeyAlgoDSA PublicKeyAlgorithm = 17 @@ -526,7 +530,8 @@ const ( // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, + case PubKeyAlgoAEAD, PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, + PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return true } @@ -537,7 +542,8 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { // sign a message. func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, + case PubKeyAlgoAEAD, PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, + PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: diff --git a/openpgp/packet/padding.go b/openpgp/packet/padding.go index 3b6a7045d..da9a46eae 100644 --- a/openpgp/packet/padding.go +++ b/openpgp/packet/padding.go @@ -17,7 +17,7 @@ func (pad Padding) parse(reader io.Reader) error { // SerializePadding writes the padding to writer. func (pad Padding) SerializePadding(writer io.Writer, rand io.Reader) error { - err := serializeHeader(writer, packetPadding, int(pad)) + err := serializeHeader(writer, packetTypePadding, int(pad)) if err != nil { return err } diff --git a/openpgp/packet/persistent_symmetric_key.go b/openpgp/packet/persistent_symmetric_key.go new file mode 100644 index 000000000..61b515f61 --- /dev/null +++ b/openpgp/packet/persistent_symmetric_key.go @@ -0,0 +1,183 @@ +// Copyright 2026 Proton AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package packet + +import ( + "bytes" + "crypto" + "hash" + "io" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/s2k" + "golang.org/x/crypto/hkdf" +) + +type PersistentSymmetricKeyPublicFields struct { + SymmetricAlgorithm CipherFunction + FingerprintSeed []byte +} + +type PersistentSymmetricKeyPrivateFields struct { + Key []byte +} + +// PersistentSymmetricKey represents a persistent symmetric key packet. +// See draft-ietf-openpgp-persistent-symmetric-keys. +type PersistentSymmetricKey struct { + PrivateKey +} + +func NewPersistentSymmetricKey(creationTime time.Time, symmetricAlgorithm CipherFunction, fingerprintSeed, keyMaterial []byte) *PersistentSymmetricKey { + if len(fingerprintSeed) != 32 { + panic("openpgp: incorrect fingerprint seed length in NewPersistentSymmetricKey") + } + if len(keyMaterial) != symmetricAlgorithm.KeySize() { + panic("openpgp: incorrect key material length in NewPersistentSymmetricKey") + } + psk := &PersistentSymmetricKey{ + PrivateKey: PrivateKey{ + PublicKey: PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PubKeyAlgoAEAD, + PublicKey: &PersistentSymmetricKeyPublicFields{ + SymmetricAlgorithm: symmetricAlgorithm, + FingerprintSeed: fingerprintSeed, + }, + }, + PrivateKey: &PersistentSymmetricKeyPrivateFields{ + Key: keyMaterial, + }, + }, + } + psk.setFingerprintAndKeyId() + return psk +} + +func (psk *PersistentSymmetricKey) parse(r io.Reader) (err error) { + err = (&psk.PrivateKey).parsePrivateKey(r) + if err != nil { + return + } + if psk.Version < 6 { + return errors.StructuralError("Persistent Symmetric Key packets can only be used with version 6") + } + if psk.PubKeyAlgo != PubKeyAlgoAEAD { + return errors.StructuralError("Persistent Symmetric Key packets can only be used with algorithm 0") + } + if psk.s2kType != S2KNON && psk.s2kType != S2KAEAD { + return errors.StructuralError("Persistent Symmetric Key packets can only be encrypted with modern AEAD") + } + return +} + +func (psk *PersistentSymmetricKey) Serialize(w io.Writer) (err error) { + // Sanity checks + if psk.Version < 6 { + return errors.StructuralError("Persistent Symmetric Key packets can only be used with version 6") + } + if psk.PubKeyAlgo != PubKeyAlgoAEAD { + return errors.StructuralError("Persistent Symmetric Key packets can only be used with algorithm 0") + } + if psk.s2kType != S2KNON && psk.s2kType != S2KAEAD { + return errors.StructuralError("Persistent Symmetric Key packets can only be encrypted with modern AEAD") + } + + contents := bytes.NewBuffer(nil) + err = (&psk.PrivateKey).serializeWithoutHeaders(contents) + if err != nil { + return + } + + err = serializeHeader(w, packetTypePersistentSymmetricKey, contents.Len()) + if err != nil { + return + } + _, err = io.Copy(w, contents) + if err != nil { + return + } + return +} + +// EncryptWithConfig encrypts an unencrypted persistent symmetric key using the passphrase and the config. +func (psk *PersistentSymmetricKey) EncryptWithConfig(passphrase []byte, config *Config) error { + params, err := s2k.Generate(config.Random(), config.S2K()) + if err != nil { + return err + } + // Derive an encryption key with the configured s2k function. + key := make([]byte, config.Cipher().KeySize()) + s2k, err := params.Function() + if err != nil { + return err + } + s2k(key, passphrase) + s2kType := S2KAEAD + psk.aead = config.AEAD().Mode() + psk.cipher = config.Cipher() + key = psk.applyHKDF(key) + // Encrypt the persistent symmetric key with the derived encryption key. + return psk.encrypt(key, params, s2kType, config.Cipher(), config.Random()) +} + +// Encrypt encrypts an unencrypted persistent symmetric key using a passphrase. +func (psk *PersistentSymmetricKey) Encrypt(passphrase []byte) error { + // Default config of persistent symmetric key encryption + config := &Config{ + S2KConfig: &s2k.Config{ + S2KMode: s2k.Argon2S2K, + }, + DefaultCipher: CipherAES256, + } + return psk.EncryptWithConfig(passphrase, config) +} + +// VerifySignature returns nil iff sig is a valid signature, made by this +// key, of the data hashed into signed. signed is mutated by this call. +func (psk *PersistentSymmetricKey) VerifySignature(signed hash.Hash, sig *Signature) (err error) { + signed.Write(sig.HashSuffix) + hashBytes := signed.Sum(nil) + if hashBytes[0] != sig.HashTag[0] || hashBytes[1] != sig.HashTag[1] { + return errors.SignatureError("hash tag doesn't match") + } + + if sig.PubKeyAlgo != PubKeyAlgoAEAD { + return errors.SignatureError("persistent symmetric keys can only verify AEAD signatures") + } + + pk := psk.PublicKey.PublicKey.(*PersistentSymmetricKeyPublicFields) + sk := psk.PrivateKey.PrivateKey.(*PersistentSymmetricKeyPrivateFields) + packetID := 0xC0 | packetTypeSignature + version := sig.Version + aeadMode := *sig.AEADMode + info := []byte{byte(packetID), byte(version), byte(pk.SymmetricAlgorithm), byte(aeadMode)} + salt := sig.SigBytes1 + hkdf := hkdf.New(crypto.SHA512.New, sk.Key, salt, info) + keySize := pk.SymmetricAlgorithm.KeySize() + ivLength := aeadMode.IvLength() + encKey := make([]byte, keySize) + iv := make([]byte, ivLength) + _, err = hkdf.Read(encKey) + if err != nil { + return err + } + _, err = hkdf.Read(iv) + if err != nil { + return err + } + authTag := sig.SigBytes2 + modeInstance := aeadMode.new(pk.SymmetricAlgorithm.new(encKey)) + result, err := modeInstance.Open(nil, iv, authTag, hashBytes) + if err != nil { + return err + } + if len(result) != 0 { + return errors.SignatureError("unexpected plaintext in AEAD signature") + } + return nil +} diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index c048138ad..3aaa6dfe1 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -47,7 +47,8 @@ type PrivateKey struct { s2k func(out, in []byte) aead AEADMode // only relevant if S2KAEAD is enabled // An *{rsa|dsa|elgamal|ecdh|ecdsa|ed25519|ed448}.PrivateKey or - // crypto.Signer/crypto.Decrypter (Decryptor RSA only). + // crypto.Signer/crypto.Decrypter (Decryptor RSA only) or + // *packet.PersistentSymmetricKeyPrivateFields (AEAD only). PrivateKey interface{} iv []byte @@ -205,7 +206,18 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv } func (pk *PrivateKey) parse(r io.Reader) (err error) { - err = (&pk.PublicKey).parse(r) + err = pk.parsePrivateKey(r) + if err != nil { + return + } + if pk.PubKeyAlgo == PubKeyAlgoAEAD { + return goerrors.New("openpgp: AEAD may only be used with persistent symmetric key packets") + } + return +} + +func (pk *PrivateKey) parsePrivateKey(r io.Reader) (err error) { + err = (&pk.PublicKey).parsePublicKey(r) if err != nil { return } @@ -351,10 +363,10 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return errors.StructuralError("private key checksum failure") } privateKeyData = privateKeyData[:len(privateKeyData)-2] - return pk.parsePrivateKey(privateKeyData) + return pk.parsePrivateKeyMaterial(privateKeyData) } else { // No checksum - return pk.parsePrivateKey(privateKeyData) + return pk.parsePrivateKeyMaterial(privateKeyData) } } @@ -377,11 +389,32 @@ func mod64kHash(d []byte) uint16 { func (pk *PrivateKey) Serialize(w io.Writer) (err error) { contents := bytes.NewBuffer(nil) - err = pk.PublicKey.serializeWithoutHeaders(contents) + err = pk.serializeWithoutHeaders(contents) if err != nil { return } - if _, err = contents.Write([]byte{uint8(pk.s2kType)}); err != nil { + + ptype := packetTypePrivateKey + if pk.IsSubkey { + ptype = packetTypePrivateSubkey + } + err = serializeHeader(w, ptype, contents.Len()) + if err != nil { + return + } + _, err = io.Copy(w, contents) + if err != nil { + return + } + return +} + +func (pk *PrivateKey) serializeWithoutHeaders(w io.Writer) (err error) { + err = pk.PublicKey.serializeWithoutHeaders(w) + if err != nil { + return + } + if _, err = w.Write([]byte{uint8(pk.s2kType)}); err != nil { return } @@ -434,10 +467,10 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) { } } if pk.Version == 5 || (pk.Version == 6 && pk.s2kType != S2KNON) { - contents.Write([]byte{uint8(optional.Len())}) + w.Write([]byte{uint8(optional.Len())}) } - if _, err := io.Copy(contents, optional); err != nil { + if _, err := io.Copy(w, optional); err != nil { return err } @@ -446,7 +479,7 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) { var priv []byte if !pk.Encrypted { buf := bytes.NewBuffer(nil) - err = pk.serializePrivateKey(buf) + err = pk.serializePrivateKeyMaterial(buf) if err != nil { return err } @@ -461,26 +494,18 @@ func (pk *PrivateKey) Serialize(w io.Writer) (err error) { } if pk.Version == 5 { - contents.Write([]byte{byte(l >> 24), byte(l >> 16), byte(l >> 8), byte(l)}) + w.Write([]byte{byte(l >> 24), byte(l >> 16), byte(l >> 8), byte(l)}) } - contents.Write(priv) - } - - ptype := packetTypePrivateKey - if pk.IsSubkey { - ptype = packetTypePrivateSubkey - } - err = serializeHeader(w, ptype, contents.Len()) - if err != nil { - return - } - _, err = io.Copy(w, contents) - if err != nil { - return + w.Write(priv) } return } +func serializeAEADPrivateKey(w io.Writer, priv *PersistentSymmetricKeyPrivateFields) error { + _, err := w.Write(priv.Key) + return err +} + func serializeRSAPrivateKey(w io.Writer, priv *rsa.PrivateKey) error { if _, err := w.Write(new(encoding.MPI).SetBig(priv.D).EncodedBytes()); err != nil { return err @@ -629,7 +654,7 @@ func (pk *PrivateKey) decrypt(decryptionKey []byte) error { return errors.InvalidArgumentError("invalid s2k type") } - err := pk.parsePrivateKey(data) + err := pk.parsePrivateKeyMaterial(data) if _, ok := err.(errors.KeyInvalidError); ok { return errors.KeyInvalidError("invalid key parameters") } @@ -718,7 +743,7 @@ func (pk *PrivateKey) encrypt(key []byte, params *s2k.Params, s2kType S2KType, c } priv := bytes.NewBuffer(nil) - err := pk.serializePrivateKey(priv) + err := pk.serializePrivateKeyMaterial(priv) if err != nil { return err } @@ -852,8 +877,10 @@ func (pk *PrivateKey) Encrypt(passphrase []byte) error { return pk.EncryptWithConfig(passphrase, config) } -func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { +func (pk *PrivateKey) serializePrivateKeyMaterial(w io.Writer) (err error) { switch priv := pk.PrivateKey.(type) { + case *PersistentSymmetricKeyPrivateFields: + err = serializeAEADPrivateKey(w, priv) case *rsa.PrivateKey: err = serializeRSAPrivateKey(w, priv) case *dsa.PrivateKey: @@ -886,8 +913,10 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { return } -func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { +func (pk *PrivateKey) parsePrivateKeyMaterial(data []byte) (err error) { switch pk.PublicKey.PubKeyAlgo { + case PubKeyAlgoAEAD: + return pk.parseAEADPrivateKey(data) case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoRSAEncryptOnly: return pk.parseRSAPrivateKey(data) case PubKeyAlgoDSA: @@ -939,6 +968,13 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { } } +func (pk *PrivateKey) parseAEADPrivateKey(data []byte) (err error) { + aeadPriv := new(PersistentSymmetricKeyPrivateFields) + aeadPriv.Key = data + pk.PrivateKey = aeadPriv + return nil +} + func (pk *PrivateKey) parseRSAPrivateKey(data []byte) (err error) { rsaPub := pk.PublicKey.PublicKey.(*rsa.PublicKey) rsaPriv := new(rsa.PrivateKey) @@ -1165,7 +1201,9 @@ func (pk *PrivateKey) additionalData() ([]byte, error) { additionalData := bytes.NewBuffer(nil) // Write additional data prefix based on packet type var packetByte byte - if pk.PublicKey.IsSubkey { + if pk.PubKeyAlgo == PubKeyAlgoAEAD { + packetByte = 0xe8 // Must be a persistent symmetric key packet + } else if pk.PublicKey.IsSubkey { packetByte = 0xc7 } else { packetByte = 0xc5 @@ -1184,7 +1222,9 @@ func (pk *PrivateKey) additionalData() ([]byte, error) { func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte { var packetByte byte - if pk.PublicKey.IsSubkey { + if pk.PubKeyAlgo == PubKeyAlgoAEAD { + packetByte = 0xe8 // Must be a persistent symmetric key packet + } else if pk.PublicKey.IsSubkey { packetByte = 0xc7 } else { packetByte = 0xc5 diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index bff1a6b11..5bcf705fb 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,6 +5,7 @@ package packet import ( + "bytes" "crypto/dsa" "crypto/rsa" "crypto/sha1" @@ -80,6 +81,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 { @@ -278,6 +299,17 @@ func NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKe } func (pk *PublicKey) parse(r io.Reader) (err error) { + err = pk.parsePublicKey(r) + if err != nil { + return + } + if pk.PubKeyAlgo == PubKeyAlgoAEAD { + return goerrors.New("openpgp: AEAD may only be used with persistent symmetric key packets") + } + return +} + +func (pk *PublicKey) parsePublicKey(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte _, err = readFull(r, buf[:]) @@ -307,6 +339,8 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5]) // Ignore four-octet length switch pk.PubKeyAlgo { + case PubKeyAlgoAEAD: + err = pk.parseAEAD(r) case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: err = pk.parseRSA(r) case PubKeyAlgoDSA: @@ -403,6 +437,27 @@ func (pk *PublicKey) checkV6Compatibility() error { return nil } +// parseAEAD parses AEAD public key material from the given Reader. +// See draft-ietf-openpgp-persistent-symmetric-keys. +func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { + algoAndFpSeed := make([]byte, 33) + _, err = io.ReadFull(r, algoAndFpSeed) + if err != nil { + return + } + aead := &PersistentSymmetricKeyPublicFields{ + SymmetricAlgorithm: CipherFunction(algoAndFpSeed[0]), + FingerprintSeed: algoAndFpSeed[1:], + } + if aead.SymmetricAlgorithm != CipherAES128 && + aead.SymmetricAlgorithm != CipherAES192 && + aead.SymmetricAlgorithm != CipherAES256 { + return errors.UnsupportedError(fmt.Sprintf("unknown or weak algorithm: %d", aead.SymmetricAlgorithm)) + } + pk.PublicKey = aead + return +} + // parseRSA parses RSA public key material from the given Reader. See RFC 4880, // section 5.5.2. func (pk *PublicKey) parseRSA(r io.Reader) (err error) { @@ -546,11 +601,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 { @@ -561,10 +618,23 @@ 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 } @@ -815,6 +885,8 @@ func (pk *PublicKey) Serialize(w io.Writer) (err error) { func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length := uint32(0) switch pk.PubKeyAlgo { + case PubKeyAlgoAEAD: + length += 33 // Symmetric algorithm ID and fingerprint seed case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: length += uint32(pk.n.EncodedLength()) length += uint32(pk.e.EncodedLength()) @@ -888,6 +960,13 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } switch pk.PubKeyAlgo { + case PubKeyAlgoAEAD: + publicKey := pk.PublicKey.(*PersistentSymmetricKeyPublicFields) + if _, err = w.Write([]byte{byte(publicKey.SymmetricAlgorithm)}); err != nil { + return + } + _, err = w.Write(publicKey.FingerprintSeed) + return case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: if _, err = w.Write(pk.n.EncodedBytes()); err != nil { return @@ -1061,25 +1140,25 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return nil case PubKeyAlgoEd25519: ed25519PublicKey := pk.PublicKey.(*ed25519.PublicKey) - if !ed25519.Verify(ed25519PublicKey, hashBytes, sig.EdSig) { + if !ed25519.Verify(ed25519PublicKey, hashBytes, sig.SigBytes1) { return errors.SignatureError("Ed25519 verification failure") } return nil case PubKeyAlgoEd448: ed448PublicKey := pk.PublicKey.(*ed448.PublicKey) - if !ed448.Verify(ed448PublicKey, hashBytes, sig.EdSig) { + if !ed448.Verify(ed448PublicKey, hashBytes, sig.SigBytes1) { return errors.SignatureError("ed448 verification failure") } return nil case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: mldsaEddsaPublicKey := pk.PublicKey.(*mldsa_eddsa.PublicKey) - if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.MldsaSig, sig.EdSig) { + if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.SigBytes2, sig.SigBytes1) { return errors.SignatureError("MldsaEddsa verification failure") } return nil case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: slhDsaPublicKey := pk.PublicKey.(*slhdsa.PublicKey) - if !slhdsa.Verify(slhDsaPublicKey, hashBytes, sig.SlhdsaSig) { + if !slhdsa.Verify(slhDsaPublicKey, hashBytes, sig.SigBytes1) { return errors.SignatureError("Slhdsa verification failure") } return nil @@ -1153,6 +1232,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 } diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 1bfcb21c7..8f067872b 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -27,6 +27,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/cloudflare/circl/sign/mldsa/mldsa87" + "golang.org/x/crypto/hkdf" ) const ( @@ -38,7 +39,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -84,9 +85,8 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field - EdSig []byte - MldsaSig []byte - SlhdsaSig []byte + SigBytes1, SigBytes2 []byte + AEADMode *AEADMode // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -135,8 +135,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. @@ -206,7 +207,7 @@ 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, + case PubKeyAlgoAEAD, PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, @@ -309,6 +310,24 @@ func (sig *Signature) parse(r io.Reader) (err error) { } switch sig.PubKeyAlgo { + case PubKeyAlgoAEAD: + aeadModeAndSalt := make([]byte, 1 + 32) + _, err = readFull(r, aeadModeAndSalt) + if err != nil { + return + } + aeadMode := AEADMode(aeadModeAndSalt[0]) + if !aeadMode.IsSupported() { + return errors.UnsupportedError("unsupported AEAD mode in signature") + } + sig.AEADMode = &aeadMode + sig.SigBytes1 = aeadModeAndSalt[1:] + authTag := make([]byte, aeadMode.TagLength()) + _, err = readFull(r, authTag) + if err != nil { + return + } + sig.SigBytes2 = authTag case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: sig.RSASignature = new(encoding.MPI) _, err = sig.RSASignature.ReadFrom(r) @@ -339,12 +358,12 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } case PubKeyAlgoEd25519: - sig.EdSig, err = ed25519.ReadSignature(r) + sig.SigBytes1, err = ed25519.ReadSignature(r) if err != nil { return } case PubKeyAlgoEd448: - sig.EdSig, err = ed448.ReadSignature(r) + sig.SigBytes1, err = ed448.ReadSignature(r) if err != nil { return } @@ -369,13 +388,13 @@ func (sig *Signature) parse(r io.Reader) (err error) { // parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in // https://www.rfc-editor.org/rfc/rfc9980.html#name-signature-packet-packet-typ func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (err error) { - sig.EdSig = make([]byte, ecLen) - if _, err = io.ReadFull(r, sig.EdSig); err != nil { + sig.SigBytes1 = make([]byte, ecLen) + if _, err = io.ReadFull(r, sig.SigBytes1); err != nil { return } - sig.MldsaSig = make([]byte, dLen) - _, err = io.ReadFull(r, sig.MldsaSig) + sig.SigBytes2 = make([]byte, dLen) + _, err = io.ReadFull(r, sig.SigBytes2) return } @@ -385,8 +404,8 @@ func (sig *Signature) parseSlhdsaSignature(r io.Reader, algID PublicKeyAlgorithm if err != nil { return err } - sig.SlhdsaSig = make([]byte, scheme.SignatureSize()) - _, err = io.ReadFull(r, sig.SlhdsaSig) + sig.SigBytes1 = make([]byte, scheme.SignatureSize()) + _, err = io.ReadFull(r, sig.SigBytes1) return } @@ -630,6 +649,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 } @@ -992,6 +1014,37 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e return } switch priv.PubKeyAlgo { + case PubKeyAlgoAEAD: + pk := priv.PublicKey.PublicKey.(*PersistentSymmetricKeyPublicFields) + sk := priv.PrivateKey.(*PersistentSymmetricKeyPrivateFields) + packetID := 0xC0 | packetTypeSignature + version := sig.Version + aeadMode := config.AEAD().Mode() + info := []byte{byte(packetID), byte(version), byte(pk.SymmetricAlgorithm), byte(aeadMode)} + salt := make([]byte, 32) + _, err := io.ReadFull(config.Random(), salt) + if err != nil { + return err + } + hkdf := hkdf.New(crypto.SHA512.New, sk.Key, salt, info) + keySize := pk.SymmetricAlgorithm.KeySize() + ivLength := aeadMode.IvLength() + encKey := make([]byte, keySize) + iv := make([]byte, ivLength) + _, err = hkdf.Read(encKey) + if err != nil { + return err + } + _, err = hkdf.Read(iv) + if err != nil { + return err + } + modeInstance := aeadMode.new(pk.SymmetricAlgorithm.new(encKey)) + var authTag []byte + authTag = modeInstance.Seal(authTag, iv, []byte{}, digest) + sig.SigBytes1 = salt + sig.SigBytes2 = authTag + sig.AEADMode = &aeadMode case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: // supports both *rsa.PrivateKey and crypto.Signer sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, sig.Hash) @@ -1038,13 +1091,13 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sk := priv.PrivateKey.(*ed25519.PrivateKey) signature, err := ed25519.Sign(sk, digest) if err == nil { - sig.EdSig = signature + sig.SigBytes1 = signature } case PubKeyAlgoEd448: sk := priv.PrivateKey.(*ed448.PrivateKey) signature, err := ed448.Sign(sk, digest) if err == nil { - sig.EdSig = signature + sig.SigBytes1 = signature } case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: if sig.Version != 6 { @@ -1054,8 +1107,8 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e dSig, ecSig, err := mldsa_eddsa.Sign(sk, digest) if err == nil { - sig.MldsaSig = dSig - sig.EdSig = ecSig + sig.SigBytes1 = ecSig + sig.SigBytes2 = dSig } case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: if sig.Version != 6 { @@ -1065,7 +1118,7 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e dSig, err := slhdsa.Sign(sk, digest) if err == nil { - sig.SlhdsaSig = dSig + sig.SigBytes1 = dSig } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) @@ -1184,12 +1237,14 @@ 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 && sig.SlhdsaSig == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.SigBytes1 == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } sigLength := 0 switch sig.PubKeyAlgo { + case PubKeyAlgoAEAD: + sigLength = 1 /* AEAD algorithm */ + len(sig.SigBytes1) + len(sig.SigBytes2) case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: sigLength = int(sig.RSASignature.EncodedLength()) case PubKeyAlgoDSA: @@ -1206,10 +1261,10 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { case PubKeyAlgoEd448: sigLength = ed448.SignatureSize case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: - sigLength = len(sig.EdSig) - sigLength += len(sig.MldsaSig) + sigLength = len(sig.SigBytes1) + sigLength += len(sig.SigBytes2) case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: - sigLength += len(sig.SlhdsaSig) + sigLength += len(sig.SigBytes1) default: panic("impossible") } @@ -1295,6 +1350,14 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { } switch sig.PubKeyAlgo { + case PubKeyAlgoAEAD: + if _, err = w.Write([]byte{byte(*sig.AEADMode)}); err != nil { + return + } + if _, err = w.Write(sig.SigBytes1); err != nil { + return + } + _, err = w.Write(sig.SigBytes2) case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly: _, err = w.Write(sig.RSASignature.EncodedBytes()) case PubKeyAlgoDSA: @@ -1313,16 +1376,16 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { } _, err = w.Write(sig.EdDSASigS.EncodedBytes()) case PubKeyAlgoEd25519: - err = ed25519.WriteSignature(w, sig.EdSig) + err = ed25519.WriteSignature(w, sig.SigBytes1) case PubKeyAlgoEd448: - err = ed448.WriteSignature(w, sig.EdSig) + err = ed448.WriteSignature(w, sig.SigBytes1) case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: - if _, err = w.Write(sig.EdSig); err != nil { + if _, err = w.Write(sig.SigBytes1); err != nil { return } - _, err = w.Write(sig.MldsaSig) + _, err = w.Write(sig.SigBytes2) case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: - _, err = w.Write(sig.SlhdsaSig) + _, err = w.Write(sig.SigBytes1) default: panic("impossible") } @@ -1437,6 +1500,9 @@ func (sig *Signature) buildSubpackets(config *Config) (subpackets []outputSubpac if sig.FlagAuthenticate { flags |= KeyFlagAuthenticate } + if sig.FlagForward { + flags |= KeyFlagForward + } if sig.FlagGroupKey { flags |= KeyFlagGroupKey } 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 3b273cca4..9f30c3560 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -125,6 +125,34 @@ func newEntity(uid *userIdData, config *packet.Config) (*Entity, error) { return e, nil } +// NewSymmetricEntity returns an Entity that contains a fresh persistent +// symmetric key for signing and encrypting pgp messages. The key is not +// associated with an identity. It always returns a v6 persistent symmetric +// key, regardless of the config. +func NewSymmetricEntity(config *packet.Config) (*Entity, error) { + creationTime := config.Now() + symmetricAlgorithm := config.Cipher() + rand := config.Random() + + fingerprintSeed := make([]byte, 32) + rand.Read(fingerprintSeed) + + keyMaterial := make([]byte, symmetricAlgorithm.KeySize()) + rand.Read(keyMaterial) + + psk := packet.NewPersistentSymmetricKey(creationTime, symmetricAlgorithm, fingerprintSeed, keyMaterial) + e := &Entity{ + PSK: psk, + PrimaryKey: &psk.PublicKey, + PrivateKey: &psk.PrivateKey, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + DirectSignatures: []*packet.VerifiableSignature{}, + } + + return e, nil +} + // AddUserId adds a user-id packet to the given entity. func (t *Entity) AddUserId(name, comment, email string, config *packet.Config) error { var keyProperties *keyProperties diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index 5aca5655c..f46b25e60 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -27,6 +27,7 @@ var PrivateKeyType = "PGP PRIVATE KEY BLOCK" type Entity struct { PrimaryKey *packet.PublicKey PrivateKey *packet.PrivateKey + PSK *packet.PersistentSymmetricKey Identities map[string]*Identity // indexed by Identity.Name Revocations []*packet.VerifiableSignature DirectSignatures []*packet.VerifiableSignature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6)} @@ -40,6 +41,7 @@ type Key struct { PrimarySelfSignature *packet.Signature // might be nil, if not verified PublicKey *packet.PublicKey PrivateKey *packet.PrivateKey + PSK *packet.PersistentSymmetricKey SelfSignature *packet.Signature // might be nil, if not verified } @@ -106,6 +108,11 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) // given Entity. // Provides an error if the function fails to find an encryption key. func (e *Entity) EncryptionKeyWithError(now time.Time, config *packet.Config) (Key, error) { + // If e contains a persistent symmetric key, return that + if e.PSK != nil { + return Key{e, nil, e.PrimaryKey, e.PrivateKey, e.PSK, nil}, nil + } + // The primary key has to be valid at time now primarySelfSignature, err := e.VerifyPrimaryKey(now, config) if err != nil { // primary key is not valid @@ -202,6 +209,13 @@ func (e *Entity) EncryptionKeyWithError(now time.Time, config *packet.Config) (K // If id is 0 all decryption keys are returned. // This is useful to retrieve keys for session key decryption. func (e *Entity) DecryptionKeys(id uint64, date time.Time, config *packet.Config) (keys []Key) { + // If e contains a persistent symmetric key, return that + if e.PSK != nil && + (id == 0 || e.PSK.KeyId == id) { + keys = append(keys, Key{e, nil, e.PrimaryKey, e.PrivateKey, e.PSK, nil}) + return + } + primarySelfSignature, err := e.PrimarySelfSignature(date, config) if err != nil { // primary key is not valid return @@ -209,13 +223,14 @@ 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}) + keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, nil, subkeySelfSig}) } } - if config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo, config) { - keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) + if (config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo, config)) && + (id == 0 || e.PrimaryKey.KeyId == id) { + keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, nil, primarySelfSignature}) } return } @@ -247,6 +262,11 @@ func (e *Entity) SigningKeyById(now time.Time, id uint64, config *packet.Config) } func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config *packet.Config) (Key, error) { + // If e contains a persistent symmetric key, return that + if e.PSK != nil { + return Key{e, nil, e.PrimaryKey, e.PrivateKey, e.PSK, nil}, nil + } + primarySelfSignature, err := e.VerifyPrimaryKey(now, config) if err != nil { return Key{}, err @@ -337,6 +357,10 @@ func (e *Entity) Revoked(now time.Time) bool { // derived from the provided passphrase. Public keys and dummy keys are ignored, // and don't cause an error to be returned. func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) error { + if e.PSK != nil { + return e.PSK.EncryptWithConfig(passphrase, config) + } + var keysToEncrypt []*packet.PrivateKey // Add entity private key to encrypt. if e.PrivateKey != nil && !e.PrivateKey.Dummy() && !e.PrivateKey.Encrypted { @@ -356,6 +380,10 @@ func (e *Entity) EncryptPrivateKeys(passphrase []byte, config *packet.Config) er // Avoids recomputation of similar s2k key derivations. Public keys and dummy keys are ignored, // and don't cause an error to be returned. func (e *Entity) DecryptPrivateKeys(passphrase []byte) error { + if e.PSK != nil { + return e.PSK.Decrypt(passphrase) + } + var keysToDecrypt []*packet.PrivateKey // Add entity private key to decrypt. if e.PrivateKey != nil && !e.PrivateKey.Dummy() && e.PrivateKey.Encrypted { @@ -380,12 +408,12 @@ type EntityList []*Entity func (el EntityList) KeysById(id uint64) (keys []Key) { for _, e := range el { if id == 0 || e.PrimaryKey.KeyId == id { - keys = append(keys, Key{e, nil, e.PrimaryKey, e.PrivateKey, nil}) + keys = append(keys, Key{e, nil, e.PrimaryKey, e.PrivateKey, e.PSK, nil}) } for _, subKey := range e.Subkeys { if id == 0 || subKey.PublicKey.KeyId == id { - keys = append(keys, Key{subKey.Primary, nil, subKey.PublicKey, subKey.PrivateKey, nil}) + keys = append(keys, Key{subKey.Primary, nil, subKey.PublicKey, subKey.PrivateKey, nil, nil}) } } } @@ -439,11 +467,11 @@ func ReadKeyRing(r io.Reader) (el EntityList, err error) { // TODO: warn about skipped unsupported/unreadable keys if _, ok := err.(errors.UnsupportedError); ok { lastUnsupportedError = err - err = readToNextPublicKey(packets) + err = readToNextPrimaryKey(packets) } else if _, ok := err.(errors.StructuralError); ok { // Skip unreadable, badly-formatted keys lastUnsupportedError = err - err = readToNextPublicKey(packets) + err = readToNextPrimaryKey(packets) } if err == io.EOF { err = nil @@ -464,9 +492,9 @@ func ReadKeyRing(r io.Reader) (el EntityList, err error) { return } -// readToNextPublicKey reads packets until the start of the entity and leaves +// readToNextPrimaryKey reads packets until the start of the next entity and leaves // the first packet of the new entity in the Reader. -func readToNextPublicKey(packets *packet.Reader) (err error) { +func readToNextPrimaryKey(packets *packet.Reader) (err error) { var p packet.Packet for { p, err = packets.Next() @@ -483,6 +511,10 @@ func readToNextPublicKey(packets *packet.Reader) (err error) { packets.Unread(p) return } + if _, ok := p.(*packet.PersistentSymmetricKey); ok { + packets.Unread(p) + return + } } } @@ -498,6 +530,12 @@ func ReadEntity(packets *packet.Reader) (*Entity, error) { } var ok bool + if e.PSK, ok = p.(*packet.PersistentSymmetricKey); ok { + e.PrimaryKey = &e.PSK.PublicKey + e.PrivateKey = &e.PSK.PrivateKey + // Persistent Symmetric Keys don't have any subcomponents. + return e, nil + } if e.PrimaryKey, ok = p.(*packet.PublicKey); !ok { if e.PrivateKey, ok = p.(*packet.PrivateKey); !ok { packets.Unread(p) @@ -577,6 +615,9 @@ EachPacket: if err != nil { return nil, err } + case *packet.PersistentSymmetricKey: + packets.Unread(p) + break EachPacket default: // we ignore unknown packets } @@ -613,9 +654,14 @@ func (e *Entity) SerializePrivateWithoutSigning(w io.Writer, config *packet.Conf } func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign bool) (err error) { + if e.PSK != nil { + return e.PSK.Serialize(w) + } + if e.PrivateKey == nil { return goerrors.New("openpgp: private key is missing") } + err = e.PrivateKey.Serialize(w) if err != nil { return @@ -656,6 +702,10 @@ 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.PSK != nil { + return goerrors.New("openpgp: can't serialize public key of persistent symmetric key") + } + if err := e.PrimaryKey.Serialize(w); err != nil { return err } @@ -676,6 +726,11 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // Don't export public key for forwarding keys, see forwarding section 4.1. + subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{}, nil) + if err == nil && subKeySelfSig.FlagForward { + continue + } if err := subkey.Serialize(w, false); err != nil { return err } @@ -687,6 +742,10 @@ func (e *Entity) Serialize(w io.Writer) error { // specified reason code and text (RFC4880 section-5.2.3.23). // If config is nil, sensible defaults will be used. func (e *Entity) Revoke(reason packet.ReasonForRevocation, reasonText string, config *packet.Config) error { + if e.PSK != nil { + return errors.InvalidArgumentError("can't revoke persistent symmetric key") + } + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeKeyRevocation, config) revSig.RevocationReason = &reason revSig.RevocationReasonText = reasonText @@ -707,6 +766,10 @@ func (e *Entity) Revoke(reason packet.ReasonForRevocation, reasonText string, co // necessary. // If config is nil, sensible defaults will be used. func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Config) error { + if e.PSK != nil { + return errors.InvalidArgumentError("can't sign persistent symmetric key") + } + ident, ok := e.Identities[identity] if !ok { return errors.InvalidArgumentError("given identity string not found in Entity") @@ -716,6 +779,10 @@ func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Co // LatestValidDirectSignature returns the latest valid direct key-signature of the entity. func (e *Entity) LatestValidDirectSignature(date time.Time, config *packet.Config) (selectedSig *packet.Signature, err error) { + if e.PSK != nil { + return nil, nil + } + for sigIdx := len(e.DirectSignatures) - 1; sigIdx >= 0; sigIdx-- { sig := e.DirectSignatures[sigIdx] if (date.IsZero() || date.Unix() >= sig.Packet.CreationTime.Unix()) && @@ -745,6 +812,10 @@ func (e *Entity) LatestValidDirectSignature(date time.Time, config *packet.Confi // This self-signature is to be used to check the key expiration, // algorithm preferences, and so on. func (e *Entity) PrimarySelfSignature(date time.Time, config *packet.Config) (primarySig *packet.Signature, err error) { + if e.PSK != nil { + return nil, nil + } + if e.PrimaryKey.Version == 6 { return e.LatestValidDirectSignature(date, config) } @@ -761,6 +832,10 @@ func (e *Entity) PrimarySelfSignature(date time.Time, config *packet.Config) (pr // - that the primary key is not expired given its self-signature. // If date is zero (i.e., date.IsZero() == true) the time checks are not performed. func (e *Entity) VerifyPrimaryKey(date time.Time, config *packet.Config) (*packet.Signature, error) { + if e.PSK != nil { + return nil, nil + } + primarySelfSignature, err := e.PrimarySelfSignature(date, config) if err != nil { return nil, goerrors.New("no valid self signature found") @@ -787,6 +862,9 @@ func (e *Entity) VerifyPrimaryKey(date time.Time, config *packet.Config) (*packe } func (k *Key) IsPrimary() bool { + if k.PSK != nil { + return true + } if k.PrimarySelfSignature == nil || k.SelfSignature == nil { return k.PublicKey == k.Entity.PrimaryKey } @@ -852,3 +930,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/read.go b/openpgp/v2/read.go index c928b35b0..f47645ffa 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -139,8 +139,8 @@ 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.PubKeyAlgoAEAD, packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, + packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: @@ -587,7 +587,12 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { } else { candidate.SignedBy = &key } - signatureError := key.PublicKey.VerifySignature(candidate.Hash, sig) + var signatureError error + if key.PSK != nil { + signatureError = key.PSK.VerifySignature(candidate.Hash, sig) + } else { + signatureError = key.PublicKey.VerifySignature(candidate.Hash, sig) + } if signatureError == nil { signatureError = checkMessageSignatureDetails(&key, sig, scr.config) } @@ -772,7 +777,10 @@ func checkMessageSignatureDetails(verifiedKey *Key, signature *packet.Signature, return errors.ErrSignatureOlderThanKey } - sigsToCheck := []*packet.Signature{signature, verifiedKey.PrimarySelfSignature} + sigsToCheck := []*packet.Signature{signature} + if verifiedKey.PSK == nil { + sigsToCheck = append(sigsToCheck, verifiedKey.PrimarySelfSignature) + } if !verifiedKey.IsPrimary() { sigsToCheck = append(sigsToCheck, verifiedKey.SelfSignature, verifiedKey.SelfSignature.EmbeddedSignature) } diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index f167c34e2..471c53506 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -106,6 +106,17 @@ func TestDSAHashTruncatation(t *testing.T) { } } +func TestReadPersistentSymmetricKey(t *testing.T) { + kring, err := ReadKeyRing(readerFromHex(persistentSymmetricKey)) + if err != nil { + t.Error(err) + return + } + if len(kring) != 1 || uint32(kring[0].PrimaryKey.KeyId) != 0x6FBAF486 { + t.Errorf("bad parse: %#v", kring) + } +} + func TestGetKeyById(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(testKeys1And2Hex)) diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index eaf5663fa..6e9ebf5ad 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -104,6 +104,8 @@ ex7En5r7rHR5xwX82Msc+Rq9dSyO const dsaKeyWithSHA512 = `9901a2044f04b07f110400db244efecc7316553ee08d179972aab87bb1214de7692593fcf5b6feb1c80fba268722dd464748539b85b81d574cd2d7ad0ca2444de4d849b8756bad7768c486c83a824f9bba4af773d11742bdfb4ac3b89ef8cc9452d4aad31a37e4b630d33927bff68e879284a1672659b8b298222fc68f370f3e24dccacc4a862442b9438b00a0ea444a24088dc23e26df7daf8f43cba3bffc4fe703fe3d6cd7fdca199d54ed8ae501c30e3ec7871ea9cdd4cf63cfe6fc82281d70a5b8bb493f922cd99fba5f088935596af087c8d818d5ec4d0b9afa7f070b3d7c1dd32a84fca08d8280b4890c8da1dde334de8e3cad8450eed2a4a4fcc2db7b8e5528b869a74a7f0189e11ef097ef1253582348de072bb07a9fa8ab838e993cef0ee203ff49298723e2d1f549b00559f886cd417a41692ce58d0ac1307dc71d85a8af21b0cf6eaa14baf2922d3a70389bedf17cc514ba0febbd107675a372fe84b90162a9e88b14d4b1c6be855b96b33fb198c46f058568817780435b6936167ebb3724b680f32bf27382ada2e37a879b3d9de2abe0c3f399350afd1ad438883f4791e2e3b4184453412068617368207472756e636174696f6e207465737488620413110a002205024f04b07f021b03060b090807030206150802090a0b0416020301021e01021780000a0910ef20e0cefca131581318009e2bf3bf047a44d75a9bacd00161ee04d435522397009a03a60d51bd8a568c6c021c8d7cf1be8d990d6417b0020003` +const persistentSymmetricKey = `e84c066974f44e00000000210965dca189f97bd99b097cf444999b95d6fede7a195d2593d3e3a14bc46c3d43240004eddda4665267936f07f552272227ef5e952e2c45d1d163b1c7ac6b54708ab4` + const unknownHashFunctionHex = `8a00000040040001990006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` const rsaSignatureBadMPIlength = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index fb31c5bef..5f829a1a3 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -152,16 +152,18 @@ func detachSignWithWriter(w io.Writer, signers []*Entity, sigType packet.Signatu hashToHashId(crypto.SHA3_256), hashToHashId(crypto.SHA3_512), } - defaultHashes := candidateHashes[0:1] - primarySelfSignature, _ := signer.PrimarySelfSignature(config.Now(), config) - if primarySelfSignature == nil { - return nil, errors.StructuralError("signed entity has no valid self-signature") - } - preferredHashes := primarySelfSignature.PreferredHash - if len(preferredHashes) == 0 { - preferredHashes = defaultHashes + if signer.PSK == nil { + defaultHashes := candidateHashes[0:1] + primarySelfSignature, _ := signer.PrimarySelfSignature(config.Now(), config) + if primarySelfSignature == nil { + return nil, errors.StructuralError("signed entity has no valid self-signature") + } + preferredHashes := primarySelfSignature.PreferredHash + if len(preferredHashes) == 0 { + preferredHashes = defaultHashes + } + candidateHashes = intersectPreferences(candidateHashes, preferredHashes) } - candidateHashes = intersectPreferences(candidateHashes, preferredHashes) var hash crypto.Hash if hash, err = selectHash(candidateHashes, config.Hash(), signingKey.PrivateKey); err != nil { @@ -309,6 +311,11 @@ func symmetricallyEncrypt(passphrase []byte, dataWriter io.Writer, params *Encry hashToHashId(crypto.SHA3_256), hashToHashId(crypto.SHA3_512), } + if signer.PSK != nil { + // Treat Persistent Symmetric Keys as supporting all algorithms. + candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) + continue + } defaultHashes := candidateHashes[0:1] primarySelfSignature, _ := signer.PrimarySelfSignature(params.Config.Now(), params.Config) if primarySelfSignature == nil { @@ -466,10 +473,12 @@ func writeAndSign(payload io.WriteCloser, candidateHashes [][]uint8, signEntitie signer: signer, } - if signKey.PrimarySelfSignature == nil { - return nil, errors.InvalidArgumentError("signing key has no self-signature") + if signKey.PSK == nil { + if signKey.PrimarySelfSignature == nil { + return nil, errors.InvalidArgumentError("signing key has no self-signature") + } + candidateHashes[signEntityIdx] = intersectPreferences(candidateHashes[signEntityIdx], signKey.PrimarySelfSignature.PreferredHash) } - candidateHashes[signEntityIdx] = intersectPreferences(candidateHashes[signEntityIdx], signKey.PrimarySelfSignature.PreferredHash) hash, err := selectHash(candidateHashes[signEntityIdx], config.Hash(), signKey.PrivateKey) if err != nil { return nil, err @@ -594,14 +603,18 @@ 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 for _, publicRecipient := range to { if config.IntendedRecipients() { - intendedRecipients = append(intendedRecipients, &packet.Recipient{KeyVersion: publicRecipient.PrimaryKey.Version, Fingerprint: publicRecipient.PrimaryKey.Fingerprint}) + if publicRecipient.PSK != nil { + intendedRecipients = append(intendedRecipients, &packet.Recipient{KeyVersion: publicRecipient.PSK.Version, Fingerprint: publicRecipient.PSK.Fingerprint}) + } else { + intendedRecipients = append(intendedRecipients, &packet.Recipient{KeyVersion: publicRecipient.PrimaryKey.Version, Fingerprint: publicRecipient.PrimaryKey.Fingerprint}) + } } } @@ -617,6 +630,11 @@ func encrypt( return nil, err } + if encryptKeys[i].PSK != nil { + // Treat Persistent Symmetric Keys as supporting all algorithms. + continue + } + if !encryptKeys[i].PublicKey.IsPQ() { allPQ = false } @@ -693,7 +711,13 @@ func encrypt( for idx, key := range encryptKeys { // hide the keys of the hidden recipients hidden := idx >= len(to) - if err := packet.SerializeEncryptedKeyAEADwithHiddenOption(params.KeyWriter, key.PublicKey, cipher, aeadSupported, params.SessionKey, hidden, config); err != nil { + var err error + if key.PSK != nil { + err = packet.SerializeEncryptedKeyPSK(params.KeyWriter, key.PSK, cipher, aeadSupported, params.SessionKey, config) + } else { + err = packet.SerializeEncryptedKeyAEADwithHiddenOption(params.KeyWriter, key.PublicKey, cipher, aeadSupported, params.SessionKey, hidden, config) + } + if err != nil { return nil, err } } @@ -779,6 +803,11 @@ func SignWithParams(output io.Writer, signers []*Entity, params *SignParams) (in hashToHashId(crypto.SHA3_256), hashToHashId(crypto.SHA3_512), } + if signer.PSK != nil { + // Treat Persistent Symmetric Keys as supporting all algorithms. + candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) + continue + } defaultHashes := candidateHashes[0:1] primarySelfSignature, _ := signer.PrimarySelfSignature(params.Config.Now(), params.Config) if primarySelfSignature == nil { @@ -794,7 +823,6 @@ func SignWithParams(output io.Writer, signers []*Entity, params *SignParams) (in } candidateHashesPerSignature = append(candidateHashesPerSignature, candidateHashes) candidateCompression = intersectPreferences(candidateCompression, primarySelfSignature.PreferredCompression) - } sigType := packet.SigTypeBinary diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index 5e79814cd..dca4f1a24 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -727,6 +727,16 @@ var testEncryptionTests = map[string]struct { true, true, }, + "PersistentSymmetricKey": { + persistentSymmetricKey, + false, + true, + }, + "PersistentSymmetricKey_signed": { + persistentSymmetricKey, + true, + true, + }, } func TestEncryption(t *testing.T) { @@ -869,6 +879,9 @@ var testSigningTests = []struct { { ed25519wX25519Key, }, + { + persistentSymmetricKey, + }, } func TestSigning(t *testing.T) {