From 9105e9d345cef88870838309270c586782aab15d Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Tue, 24 Sep 2024 15:27:02 +0200 Subject: [PATCH 01/14] ci: Remove testing on old go versions --- .github/workflows/go.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1a05c9196..39883d5fe 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,22 +26,3 @@ jobs: - name: Randomized test suite 2 run: go test -v ./... -run RandomizeSlow -count=32 - - test-old: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Go 1.17 - uses: actions/setup-go@v3 - with: - go-version: 1.17 - - - name: Short test - run: go test -short -v ./... - - - name: Randomized test suite 1 - run: go test -v ./... -run RandomizeFast -count=512 - - - name: Randomized test suite 2 - run: go test -v ./... -run RandomizeSlow -count=32 \ No newline at end of file From afecdad127051c0738333010aec88a2b511c9b9f Mon Sep 17 00:00:00 2001 From: Kostis Andrikopoulos Date: Tue, 12 Jul 2022 18:02:19 +0200 Subject: [PATCH 02/14] openpgp: Add support for symmetric subkeys (#74) It is sometimes useful to encrypt data under some symmetric key. While this was possible to do using passphrase-derived keys, there was no support for long-term storage of the keys that was used to encrypt the key packets. To solve this, a new type of key is introduced. This key will hold a symmetric key, and will be used for both encryption and decryption of data. Specifically, as with asymmetric keys, the actual data will be encrypted using a session key, generated ad-hoc for these data. Then, instead of using a public key to encrypt the session key, the persistent symmetric key will be used instead, to produce a, so to say, Key Encrypted Key Packet. Conversly, instead of using a private key to decrypt the session key, the same symmetric key will be used. Then, the decrypted session key can be used to decrypt the data packet, as usual. As with the case of AEAD keys, it is sometimes useful to "sign" data with a persistent, symmetric key. This key holds a symmetric key, which can be used for both signing and verifying the integrity of data. While not strictly needed, the signature process will first generate a digest of the data-to-be-signed, and then the key will be used to sign the digest, using an HMAC construction. For technical reasons, related to this implenetation of the openpgp protocol, the secret key material is also stored in the newly defined public key types. Future contributors must take note of this, and not export or serialize that key in a way that it will be publicly availabe. Since symmetric keys do not have a public and private part, there is no point serializing the internal "public key" structures. Thus, symmetric keys are skipped when serialing the public part of a keyring. --- .../internal/encoding/short_byte_string.go | 50 ++++ .../encoding/short_byte_string_test.go | 61 +++++ openpgp/key_generation.go | 7 + openpgp/keys.go | 12 + openpgp/keys_test.go | 220 ++++++++++++++++++ openpgp/packet/encrypted_key.go | 60 ++++- openpgp/packet/encrypted_key_test.go | 31 +++ openpgp/packet/packet.go | 7 +- openpgp/packet/private_key.go | 91 ++++++++ openpgp/packet/public_key.go | 113 +++++++++ openpgp/packet/signature.go | 19 +- openpgp/packet/signature_test.go | 27 +++ openpgp/read.go | 2 +- openpgp/read_test.go | 9 +- openpgp/read_write_test_data.go | 18 ++ openpgp/symmetric/aead.go | 75 ++++++ openpgp/symmetric/hmac.go | 109 +++++++++ openpgp/v2/key_generation.go | 7 + openpgp/v2/keys.go | 11 + openpgp/v2/keys_test.go | 220 ++++++++++++++++++ openpgp/v2/read.go | 2 +- openpgp/v2/read_test.go | 7 + openpgp/v2/read_write_test_data.go | 18 ++ openpgp/v2/write_test.go | 85 +++++++ openpgp/write_test.go | 83 +++++++ 25 files changed, 1334 insertions(+), 10 deletions(-) create mode 100644 openpgp/internal/encoding/short_byte_string.go create mode 100644 openpgp/internal/encoding/short_byte_string_test.go create mode 100644 openpgp/symmetric/aead.go create mode 100644 openpgp/symmetric/hmac.go diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go new file mode 100644 index 000000000..0c3b91233 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string.go @@ -0,0 +1,50 @@ +package encoding + +import ( + "io" +) + +type ShortByteString struct { + length uint8 + data []byte +} + +func NewShortByteString(data []byte) *ShortByteString { + byteLength := uint8(len(data)) + + return &ShortByteString{byteLength, data} +} + +func (byteString *ShortByteString) Bytes() []byte { + return byteString.data +} + +func (byteString *ShortByteString) BitLength() uint16 { + return uint16(byteString.length) * 8 +} + +func (byteString *ShortByteString) EncodedBytes() []byte { + encodedLength := [1]byte{ + uint8(byteString.length), + } + return append(encodedLength[:], byteString.data...) +} + +func (byteString *ShortByteString) EncodedLength() uint16 { + return uint16(byteString.length) + 1 +} + +func (byteString *ShortByteString) ReadFrom(r io.Reader) (int64, error) { + var lengthBytes [1]byte + if n, err := io.ReadFull(r, lengthBytes[:]); err != nil { + return int64(n), err + } + + byteString.length = uint8(lengthBytes[0]) + + byteString.data = make([]byte, byteString.length) + if n, err := io.ReadFull(r, byteString.data); err != nil { + return int64(n + 1), err + } + return int64(byteString.length + 1), nil +} diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go new file mode 100644 index 000000000..37510a355 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var octetStreamTests = []struct { + data []byte +}{ + { + data: []byte{0x0, 0x0, 0x0}, + }, + { + data: []byte{0x1, 0x2, 0x03}, + }, + { + data: make([]byte, 255), + }, +} + +func TestShortByteString(t *testing.T) { + for i, test := range octetStreamTests { + octetStream := NewShortByteString(test.data) + + if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { + t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) + } + + expectedBitLength := uint16(len(test.data)) * 8 + if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { + t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) + } + + expectedEncodedLength := uint16(len(test.data)) + 1 + if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { + t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) + } + + encodedBytes := octetStream.EncodedBytes() + if !bytes.Equal(encodedBytes[1:], test.data) { + t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) + } + + encodedLength := int(encodedBytes[0]) + if encodedLength != len(test.data) { + t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) + } + + newStream := new(ShortByteString) + newStream.ReadFrom(bytes.NewReader(encodedBytes)) + + if !checkEquality(newStream, octetStream) { + t.Errorf("#%d: bad parsing of encoded octet stream", i) + } + } +} + +func checkEquality(left *ShortByteString, right *ShortByteString) bool { + return (left.length == right.length) && (bytes.Equal(left.data, right.data)) +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 77213f66b..b3c5c5daa 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -319,6 +320,9 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.ExperimentalPubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.HMACGenerateKey(config.Random(), hash) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -361,6 +365,9 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) + case packet.ExperimentalPubKeyAlgoAEAD: + cipher := algorithm.CipherFunction(config.Cipher()) + return symmetric.AEADGenerateKey(config.Random(), cipher) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/keys.go b/openpgp/keys.go index a071353e2..284a941cb 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -761,6 +761,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.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + return errors.InvalidArgumentError("Can't serialize symmetric primary key") + } err := e.PrimaryKey.Serialize(w) if err != nil { return err @@ -790,6 +794,14 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + + // The types of keys below are only useful as private keys. Thus, the + // public key packets contain no meaningful information and do not need + // to be serialized. + if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + continue + } err = subkey.PublicKey.Serialize(w) if err != nil { return err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 3cb4ac005..184325a61 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -1169,6 +1170,191 @@ func TestAddSubkeySerialized(t *testing.T) { } } +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + func TestAddSubkeyWithConfig(t *testing.T) { c := &packet.Config{ DefaultHash: crypto.SHA512, @@ -1865,3 +2051,37 @@ mQ00BF00000BCAD0000000000000000000000000000000000000000000000000 000000000000000000000000000000000000ABE000G0Dn000000000000000000iQ00BB0BAgAGBCG00000` ReadArmoredKeyRing(strings.NewReader(data)) } + +func TestSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index b90bb2891..6a2dbd50b 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -17,7 +17,9 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -37,6 +39,9 @@ type EncryptedKey struct { ephemeralPublicX25519 *x25519.PublicKey // used for x25519 ephemeralPublicX448 *x448.PublicKey // used for x448 encryptedSession []byte // used for x25519 and x448 + + nonce []byte + aeadMode algorithm.AEADMode } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -133,6 +138,21 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { if err != nil { return } + case ExperimentalPubKeyAlgoAEAD: + var aeadMode [1]byte + if _, err = readFull(r, aeadMode[:]); err != nil { + return + } + e.aeadMode = algorithm.AEADMode(aeadMode[0]) + nonceLength := e.aeadMode.NonceLength() + e.nonce = make([]byte, nonceLength) + if _, err = readFull(r, e.nonce); err != nil { + return + } + e.encryptedMPI1 = new(encoding.ShortByteString) + if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { + return + } } if e.Version < 6 { switch e.Algo { @@ -191,6 +211,9 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { b, err = x25519.Decrypt(priv.PrivateKey.(*x25519.PrivateKey), e.ephemeralPublicX25519, e.encryptedSession) case PubKeyAlgoX448: b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession) + case ExperimentalPubKeyAlgoAEAD: + priv := priv.PrivateKey.(*symmetric.AEADPrivateKey) + b, err = priv.Decrypt(e.nonce, e.encryptedMPI1.Bytes(), e.aeadMode) default: err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } @@ -200,7 +223,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { var key []byte switch priv.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, ExperimentalPubKeyAlgoAEAD: keyOffset := 0 if e.Version < 6 { e.CipherFunc = CipherFunction(b[0]) @@ -389,7 +412,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph var keyBlock []byte switch pub.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, ExperimentalPubKeyAlgoAEAD: lenKeyBlock := len(key) + 2 if version < 6 { lenKeyBlock += 1 // cipher type included @@ -417,7 +440,9 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoX448: return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) - case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly: + case ExperimentalPubKeyAlgoAEAD: + return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) + case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly, ExperimentalPubKeyAlgoHMAC: return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo))) } @@ -558,6 +583,35 @@ func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub * return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) } +func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte, config *AEADConfig) error { + mode := algorithm.AEADMode(config.Mode()) + iv, ciphertextRaw, err := pub.Encrypt(rand, keyBlock, mode) + if err != nil { + return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) + } + + ciphertextShortByteString := encoding.NewShortByteString(ciphertextRaw) + + buffer := append([]byte{byte(mode)}, iv...) + buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) + + packetLen := len(header) /* header length */ + packetLen += int(len(buffer)) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + + _, err = w.Write(buffer) + return err +} + func checksumKeyMaterial(key []byte) uint16 { var checksum uint16 for _, v := range key { diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index 787c7feca..5ed0a8ed3 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -16,6 +16,7 @@ import ( "crypto" "crypto/rsa" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -338,3 +339,33 @@ func TestSerializingEncryptedKey(t *testing.T) { t.Fatalf("serialization of encrypted key differed from original. Original was %s, but reserialized as %s", encryptedKeyHex, bufHex) } } + +func TestSymmetricallyEncryptedKey(t *testing.T) { + const encryptedKeyHex = "c14f03999bd17d726446da64018cb4d628ae753c646b81f87f21269cd733df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + + expectedNonce := []byte{0x8c, 0xb4, 0xd6, 0x28, 0xae, 0x75, 0x3c, 0x64, 0x6b, 0x81, 0xf8, 0x7f, 0x21, 0x26, 0x9c, 0xd7} + + expectedCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + + p, err := Read(readerFromHex(encryptedKeyHex)) + if err != nil { + t.Fatal("error reading packet") + } + + ek, ok := p.(*EncryptedKey) + if !ok { + t.Fatalf("didn't parse and EncryptedKey, got %#v", p) + } + + if ek.aeadMode != algorithm.AEADModeEAX { + t.Errorf("Parsed wrong aead mode, got %d, expected: 1", ek.aeadMode) + } + + if !bytes.Equal(expectedNonce, ek.nonce) { + t.Errorf("Parsed wrong nonce, got %x, expected %x", ek.nonce, expectedNonce) + } + + if !bytes.Equal(expectedCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedCiphertext) + } +} diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 1e92e22c9..dd4ad34c6 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -506,6 +506,9 @@ const ( PubKeyAlgoEd25519 PublicKeyAlgorithm = 27 PubKeyAlgoEd448 PublicKeyAlgorithm = 28 + ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 100 + ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 101 + // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 PubKeyAlgoRSASignOnly PublicKeyAlgorithm = 3 @@ -515,7 +518,7 @@ const ( // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, ExperimentalPubKeyAlgoAEAD: return true } return false @@ -525,7 +528,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { // sign a message. func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index f04e6c6b8..406c56e65 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -28,6 +28,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" "golang.org/x/crypto/hkdf" @@ -166,6 +167,8 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) case ed448.PrivateKey: pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) + case *symmetric.HMACPrivateKey: + pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -187,6 +190,8 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) case *x448.PrivateKey: pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) + case *symmetric.AEADPrivateKey: + pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -530,6 +535,24 @@ func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error { return err } +func serializeAEADPrivateKey(w io.Writer, priv *symmetric.AEADPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } + _, err = w.Write(priv.Key) + return +} + +func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } + _, err = w.Write(priv.Key) + return +} + // decrypt decrypts an encrypted private key using a decryption key. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -830,6 +853,10 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeEd25519PrivateKey(w, priv) case *ed448.PrivateKey: err = serializeEd448PrivateKey(w, priv) + case *symmetric.AEADPrivateKey: + err = serializeAEADPrivateKey(w, priv) + case *symmetric.HMACPrivateKey: + err = serializeHMACPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -861,6 +888,10 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { default: err = errors.StructuralError("unknown private key type") return + case ExperimentalPubKeyAlgoAEAD: + return pk.parseAEADPrivateKey(data) + case ExperimentalPubKeyAlgoHMAC: + return pk.parseHMACPrivateKey(data) } } @@ -1121,6 +1152,66 @@ func (pk *PrivateKey) applyHKDF(inputKey []byte) []byte { return encryptionKey } +func (pk *PrivateKey) parseAEADPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + aeadPriv := new(symmetric.AEADPrivateKey) + aeadPriv.PublicKey = *pubKey + + copy(aeadPriv.HashSeed[:], data[:32]) + + priv := make([]byte, pubKey.Cipher.KeySize()) + copy(priv, data[32:]) + aeadPriv.Key = priv + aeadPriv.PublicKey.Key = aeadPriv.Key + + if err = validateAEADParameters(aeadPriv); err != nil { + return + } + + pk.PrivateKey = aeadPriv + pk.PublicKey.PublicKey = &aeadPriv.PublicKey + return +} + +func (pk *PrivateKey) parseHMACPrivateKey(data []byte) (err error) { + pubKey := pk.PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + hmacPriv := new(symmetric.HMACPrivateKey) + hmacPriv.PublicKey = *pubKey + + copy(hmacPriv.HashSeed[:], data[:32]) + + priv := make([]byte, pubKey.Hash.Size()) + copy(priv, data[32:]) + hmacPriv.Key = data[32:] + hmacPriv.PublicKey.Key = hmacPriv.Key + + if err = validateHMACParameters(hmacPriv); err != nil { + return + } + + pk.PrivateKey = hmacPriv + pk.PublicKey.PublicKey = &hmacPriv.PublicKey + return +} + +func validateAEADParameters(priv *symmetric.AEADPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateHMACParameters(priv *symmetric.HMACPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { + expectedBindingHash := symmetric.ComputeBindingHash(seed) + if !bytes.Equal(expectedBindingHash, bindingHash[:]) { + return errors.KeyInvalidError("symmetric: wrong binding hash") + } + return nil +} + func validateDSAParameters(priv *dsa.PrivateKey) error { p := priv.P // group prime q := priv.Q // subgroup order diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f8da781bb..f53036510 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -28,6 +28,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -230,6 +231,30 @@ func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey return pk } +func NewAEADPublicKey(creationTime time.Time, pub *symmetric.AEADPublicKey) *PublicKey { + var pk *PublicKey + pk = &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoAEAD, + PublicKey: pub, + } + + return pk +} + +func NewHMACPublicKey(creationTime time.Time, pub *symmetric.HMACPublicKey) *PublicKey { + var pk *PublicKey + pk = &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoHMAC, + PublicKey: pub, + } + + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -280,6 +305,10 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseEd25519(r) case PubKeyAlgoEd448: err = pk.parseEd448(r) + case ExperimentalPubKeyAlgoAEAD: + err = pk.parseAEAD(r) + case ExperimentalPubKeyAlgoHMAC: + err = pk.parseHMAC(r) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -594,6 +623,58 @@ func (pk *PublicKey) parseEd448(r io.Reader) (err error) { return } +func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { + var cipher [1]byte + _, err = readFull(r, cipher[:]) + if err != nil { + return + } + + var bindingHash [32]byte + _, err = readFull(r, bindingHash[:]) + if err != nil { + return + } + + symmetric := &symmetric.AEADPublicKey{ + Cipher: algorithm.CipherFunction(cipher[0]), + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { + var hash [1]byte + _, err = readFull(r, hash[:]) + if err != nil { + return + } + bindingHash, err := readBindingHash(r) + if err != nil { + return + } + + hmacHash, ok := algorithm.HashById[hash[0]] + if !ok { + return errors.UnsupportedError("unsupported HMAC hash: " + strconv.Itoa(int(hash[0]))) + } + + symmetric := &symmetric.HMACPublicKey{ + Hash: hmacHash, + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { + _, err = readFull(r, bindingHash[:]) + return +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -681,6 +762,9 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length += ed25519.PublicKeySize case PubKeyAlgoEd448: length += ed448.PublicKeySize + case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: + length += 1 // Hash octet + length += 32 // Binding hash default: panic("unknown public key algorithm") } @@ -773,6 +857,22 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { publicKey := pk.PublicKey.(*ed448.PublicKey) _, err = w.Write(publicKey.Point) return + case ExperimentalPubKeyAlgoAEAD: + symmKey := pk.PublicKey.(*symmetric.AEADPublicKey) + cipherOctet := [1]byte{symmKey.Cipher.Id()} + if _, err = w.Write(cipherOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return + case ExperimentalPubKeyAlgoHMAC: + symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) + hashOctet := [1]byte{symmKey.Hash.Id()} + if _, err = w.Write(hashOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -859,6 +959,17 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("ed448 verification failure") } return nil + case ExperimentalPubKeyAlgoHMAC: + HMACKey := pk.PublicKey.(*symmetric.HMACPublicKey) + + result, err := HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) + if err != nil { + return err + } + if !result { + return errors.SignatureError("HMAC verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1080,6 +1191,8 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed25519.PublicKeySize * 8 case PubKeyAlgoEd448: bitLength = ed448.PublicKeySize * 8 + case ExperimentalPubKeyAlgoAEAD: + bitLength = 32 default: err = errors.InvalidArgumentError("bad public-key algorithm") } diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 3a4b366d8..28ecc33a2 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -81,6 +81,7 @@ type Signature struct { ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field EdSig []byte + HMAC encoding.Field // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -198,7 +199,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, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -336,6 +337,11 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err != nil { return } + case ExperimentalPubKeyAlgoHMAC: + sig.HMAC = new(encoding.ShortByteString) + if _, err = sig.HMAC.ReadFrom(r); err != nil { + return + } default: panic("unreachable") } @@ -996,6 +1002,11 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e if err == nil { sig.EdSig = signature } + case ExperimentalPubKeyAlgoHMAC: + sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, nil) + if err == nil { + sig.HMAC = encoding.NewShortByteString(sigdata) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1113,7 +1124,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.HMAC == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -1134,6 +1145,8 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = ed25519.SignatureSize case PubKeyAlgoEd448: sigLength = ed448.SignatureSize + case ExperimentalPubKeyAlgoHMAC: + sigLength = int(sig.HMAC.EncodedLength()) default: panic("impossible") } @@ -1240,6 +1253,8 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { err = ed25519.WriteSignature(w, sig.EdSig) case PubKeyAlgoEd448: err = ed448.WriteSignature(w, sig.EdSig) + case ExperimentalPubKeyAlgoHMAC: + _, err = w.Write(sig.HMAC.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index edc431142..19940387a 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -82,6 +82,33 @@ ltm2aQaG } } +func TestSymmetricSignatureRead(t *testing.T) { + const serializedPacket = "c272040165080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc400ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} + + packet, err := Read(readerFromHex(serializedPacket)) + if err != nil { + t.Error(err) + } + + sig, ok := packet.(*Signature) + if !ok { + t.Errorf("Did not parse a signature packet") + } + + if sig.PubKeyAlgo != ExperimentalPubKeyAlgoHMAC { + t.Error("Wrong public key algorithm") + } + + if sig.Hash != crypto.SHA256 { + t.Error("Wrong public key algorithm") + } + + if !bytes.Equal(sig.HMAC.Bytes(), expectedHMAC) { + t.Errorf("Wrong HMAC value, got: %x, expected: %x\n", sig.HMAC.Bytes(), expectedHMAC) + } +} + func TestSignatureReserialize(t *testing.T) { packet, _ := Read(readerFromHex(signatureDataHex)) sig := packet.(*Signature) diff --git a/openpgp/read.go b/openpgp/read.go index e6dd9b5fd..483857b08 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -118,7 +118,7 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD: break default: continue diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 23fd4aec1..37c2223f7 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -29,6 +29,13 @@ func readerFromHex(s string) io.Reader { return bytes.NewBuffer(data) } +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + func TestReadKeyRing(t *testing.T) { kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) if err != nil { @@ -770,7 +777,7 @@ func TestSymmetricAeadEaxOpenPGPJsMessage(t *testing.T) { } // Decrypt with key - var edp = p.(*packet.AEADEncrypted) + edp := p.(*packet.AEADEncrypted) rc, err := edp.Decrypt(packet.CipherFunction(0), key) if err != nil { panic(err) diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 670d60226..77282c0ea 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -455,3 +455,21 @@ byVJHvLO/XErtC+GNIJeMg== =liRq -----END PGP MESSAGE----- ` + +// A key that contains a persistent AEAD subkey +const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go new file mode 100644 index 000000000..b9d389dc6 --- /dev/null +++ b/openpgp/symmetric/aead.go @@ -0,0 +1,75 @@ +package symmetric + +import ( + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "io" +) + +type AEADPublicKey struct { + Cipher algorithm.CipherFunction + BindingHash [32]byte + Key []byte +} + +type AEADPrivateKey struct { + PublicKey AEADPublicKey + HashSeed [32]byte + Key []byte +} + +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv, err = generatePrivatePartAEAD(rand, cipher) + if err != nil { + return + } + + priv.generatePublicPartAEAD(cipher) + return +} + +func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv = new(AEADPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, cipher.KeySize()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *AEADPrivateKey) generatePublicPartAEAD(cipher algorithm.CipherFunction) (err error) { + priv.PublicKey.Cipher = cipher + + bindingHash := ComputeBindingHash(priv.HashSeed) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + copy(priv.PublicKey.BindingHash[:], bindingHash) + return +} + +func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AEADMode) (nonce []byte, ciphertext []byte, err error) { + block := pub.Cipher.New(pub.Key) + aead := mode.New(block) + nonce = make([]byte, aead.NonceSize()) + rand.Read(nonce) + ciphertext = aead.Seal(nil, nonce, data, nil) + return +} + +func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + + block := priv.PublicKey.Cipher.New(priv.Key) + aead := mode.New(block) + message, err = aead.Open(nil, nonce, ciphertext, nil) + return +} diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go new file mode 100644 index 000000000..e9d61475c --- /dev/null +++ b/openpgp/symmetric/hmac.go @@ -0,0 +1,109 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "crypto/sha256" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +type HMACPublicKey struct { + Hash algorithm.Hash + BindingHash [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for verification. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type HMACPrivateKey struct { + PublicKey HMACPublicKey + HashSeed [32]byte + Key []byte +} + +func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv, err = generatePrivatePartHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartHMAC(hash) + return +} + +func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv = new(HMACPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, hash.Size()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { + priv.PublicKey.Hash = hash + + bindingHash := ComputeBindingHash(priv.HashSeed) + copy(priv.PublicKey.BindingHash[:], bindingHash) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + return +} + +func ComputeBindingHash(seed [32]byte) []byte { + bindingHash := sha256.New() + bindingHash.Write(seed[:]) + + return bindingHash.Sum(nil) +} + +func (priv *HMACPrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +func (priv *HMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + expectedMAC, err := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + if err != nil { + return + } + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *HMACPublicKey) Verify(digest []byte, signature []byte) (bool, error) { + expectedMAC, err := calculateMAC(pub.Hash, pub.Key, digest) + if err != nil { + return false, err + } + return hmac.Equal(expectedMAC, signature), nil +} + +func calculateMAC(hash algorithm.Hash, key []byte, data []byte) ([]byte, error) { + hashFunc := hash.HashFunc() + if !hashFunc.Available() { + return nil, errors.UnsupportedError("hash function") + } + + mac := hmac.New(hashFunc.New, key) + mac.Write(data) + + return mac.Sum(nil), nil +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index c15eba9bb..c52d408fe 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -399,6 +400,9 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.ExperimentalPubKeyAlgoHMAC: + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.HMACGenerateKey(config.Random(), hash) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -441,6 +445,9 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) + case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + return symmetric.AEADGenerateKey(config.Random(), cipher) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index 93082dd48..15fc1b857 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -609,6 +609,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.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + return errors.InvalidArgumentError("Can't serialize symmetric primary key") + } if err := e.PrimaryKey.Serialize(w); err != nil { return err } @@ -629,6 +633,13 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // The types of keys below are only useful as private keys. Thus, the + // public key packets contain no meaningful information and do not need + // to be serialized. + if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + continue + } if err := subkey.Serialize(w, false); err != nil { return err } diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index 0b276c23e..c9d277340 100644 --- a/openpgp/v2/keys_test.go +++ b/openpgp/v2/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -2022,3 +2023,222 @@ NciH07RTRuMS/aRhRg4OB8PQROmTnZ+iZS0= t.Fatal(err) } } + +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + +func TestSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index 5ab9aff53..0f0b946fa 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -138,7 +138,7 @@ ParsePackets: switch p.Algo { case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD: break default: continue diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 4de095f01..73d9be036 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -1061,3 +1061,10 @@ func TestReadMessageWithSignOnly(t *testing.T) { t.Fatal("Should not decrypt") } } + +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 9322b949f..050598aac 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -788,3 +788,21 @@ U9I6AUkZWdcsueib9ghKDDy+HbUbf2kCJWUnuyeOCKqQifDb8bsLmdQY4Wb6 EBeLgD8oZHVsH3NLjPakPw== =STqy -----END PGP MESSAGE-----` + +// A key that contains a persistent AEAD subkey +const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index f3c4f9da7..d3c7ff48f 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -6,6 +6,7 @@ package v2 import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -997,3 +998,87 @@ FindKey: } return nil } + +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, []*Entity{entity}, msg, c) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, _, err = VerifyDetachedSignature(entityList, msg, sig, c) + if err != nil { + t.Fatal(err) + } +} diff --git a/openpgp/write_test.go b/openpgp/write_test.go index c928236b0..e2b8acb0b 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -6,6 +6,7 @@ package openpgp import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -263,6 +264,88 @@ func TestNewEntity(t *testing.T) { } } +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, entity, msg, nil) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, err = CheckDetachedSignature(entityList, msg, sig, nil) + if err != nil { + t.Fatal(err) + } +} + func TestEncryptWithCompression(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) passphrase := []byte("passphrase") From 526e449d236623f5c61fa37211526fb807bb2020 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 18 Jan 2024 15:33:40 +0100 Subject: [PATCH 03/14] Replace ioutil.ReadAll with io.ReadAll --- openpgp/v2/write_test.go | 5 ++++- openpgp/write_test.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index d3c7ff48f..285508626 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -1041,7 +1041,10 @@ func TestEncryptWithAEAD(t *testing.T) { if err != nil { t.Fatal(err) } - dec, err := ioutil.ReadAll(m.decrypted) + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } if !bytes.Equal(dec, []byte(message)) { t.Error("decrypted does not match original") diff --git a/openpgp/write_test.go b/openpgp/write_test.go index e2b8acb0b..315e73233 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -305,7 +305,10 @@ func TestEncryptWithAEAD(t *testing.T) { if err != nil { t.Fatal(err) } - dec, err := ioutil.ReadAll(m.decrypted) + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } if !bytes.Equal(dec, []byte(message)) { t.Error("decrypted does not match original") From 269eb68e221dc0c871ba51308aeefa8a4766021d Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 19 Jul 2022 12:41:32 +0200 Subject: [PATCH 04/14] Full PQC support (+33 squashed commits) Squashed commits: Update KDF to use SHA3-256 [5ff62f7] WIP: bump to draft-ietf-openpgp-pqc-01 [3949477] Import CIRCL fork with ML-KEM and ML-DSA [5033a18] Update implementation from draft v1 to v3 - Remove v6 binding for PQC KEMs - Update KDF - Update reference comments - Rename SPHINCS+ to SLH-DSA - Rename Dilithium to ML-DSA - Rename Kyber to ML-KEM - Add vectors generated with RNP - Fix misc bugs and improve tests [c53e2e3] Add benchmarking [d832873] Add read-write tests [8254a42] Bind PQC packets to v6 [21f33d3] Change testdata for Kyber keys and prepare for v6 PKESK [fa295de] Change domain separation [c5bc3c1] Add SPHINCS+ signature support [603ced6] Add references and clean code [9b26049] Prefer PQ keys [6e5ec9c] Add hybrid Kyber + ECDH, Dilithium + EC/EdDSA support [4d1ed63] Adapt PQC to the v2 API [3661202] Remove sphincs PQC logic [2a463c8] Remove PQC algorithms with brainpool and nist curves [29ee4e6] Update links to PQC draft-rfc [a75af1c] feat: Update to latest circle version [587aac2] feat: Derive ML-DSA keys from seed [ec6b930] feat: Fallback to AES256 if all recipients are PQ [1c0666f] refactor: Improve mlkem readability [5d56595] feat: Integrate review feedback [cd836af] feat: Update circl to v1.5.0 [902b302] chore: Add kmac back [cee95ab] feat: Update to new kmac key combiner in kem [086f153] Disallow v4 PQC KEM keys [2440667] feat: Add seed format for ML-KEM [3052ac2] feat: Integrate ML-DSA seed fromat [c00cd40] feat: Update kem key combinder to latest version [9677cf4] feat: Avoid panic on key size in kmac [1bd89db] fix: Kem key combiner should use the kmac correct key [28848f7] feat: Force SHA3 for ML-DSA [6faefab] feat: Enforce SHA3 in clearsing API in ML-DSA [5de74a1] refactor: Add HandleSpecificHash method on PublicKeyAlgorithm --- go.mod | 8 +- go.sum | 50 +--- internal/kmac/kmac.go | 147 ++++++++++ internal/kmac/kmac_test.go | 142 ++++++++++ openpgp/benchmark_v6_test.go | 295 +++++++++++++++++++++ openpgp/clearsign/clearsign.go | 13 +- openpgp/integration_tests/v2/utils_test.go | 14 +- openpgp/internal/ecc/curves.go | 2 + openpgp/internal/ecc/generic.go | 9 + openpgp/internal/encoding/octetarray.go | 65 +++++ openpgp/key_generation.go | 40 ++- openpgp/keys.go | 14 +- openpgp/keys_test.go | 65 +++++ openpgp/keys_v6_test.go | 141 ++++++++++ openpgp/mldsa_eddsa/mldsa_eddsa.go | 105 ++++++++ openpgp/mldsa_eddsa/mldsa_eddsa_test.go | 95 +++++++ openpgp/mlkem_ecdh/mlkem_ecdh.go | 286 ++++++++++++++++++++ openpgp/mlkem_ecdh/mlkem_ecdh_test.go | 104 ++++++++ openpgp/packet/encrypted_key.go | 92 +++++-- openpgp/packet/packet.go | 27 +- openpgp/packet/private_key.go | 117 +++++++- openpgp/packet/public_key.go | 230 +++++++++++++++- openpgp/packet/signature.go | 59 ++++- openpgp/read.go | 7 +- openpgp/read_test.go | 89 +++++++ openpgp/read_write_test_data.go | 173 ++++++++++++ openpgp/v2/key_generation.go | 38 +++ openpgp/v2/keys.go | 9 +- openpgp/v2/read.go | 9 +- openpgp/v2/read_test.go | 98 ++++++- openpgp/v2/read_write_test_data.go | 173 ++++++++++++ openpgp/v2/subkeys.go | 12 + openpgp/v2/write.go | 22 +- openpgp/v2/write_test.go | 280 ++++++++++--------- openpgp/write.go | 21 +- openpgp/write_test.go | 219 ++++++++------- 36 files changed, 2927 insertions(+), 343 deletions(-) create mode 100644 internal/kmac/kmac.go create mode 100644 internal/kmac/kmac_test.go create mode 100644 openpgp/benchmark_v6_test.go create mode 100644 openpgp/internal/encoding/octetarray.go create mode 100644 openpgp/mldsa_eddsa/mldsa_eddsa.go create mode 100644 openpgp/mldsa_eddsa/mldsa_eddsa_test.go create mode 100644 openpgp/mlkem_ecdh/mlkem_ecdh.go create mode 100644 openpgp/mlkem_ecdh/mlkem_ecdh_test.go diff --git a/go.mod b/go.mod index d417da35c..25880a800 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/ProtonMail/go-crypto -go 1.17 +go 1.22.0 require ( - github.com/cloudflare/circl v1.3.7 - golang.org/x/crypto v0.17.0 + github.com/cloudflare/circl v1.5.0 + golang.org/x/crypto v0.25.0 ) -require golang.org/x/sys v0.16.0 // indirect +require golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index 712b2d44b..1a97c0f33 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,6 @@ -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= +github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/kmac/kmac.go b/internal/kmac/kmac.go new file mode 100644 index 000000000..695ff5e42 --- /dev/null +++ b/internal/kmac/kmac.go @@ -0,0 +1,147 @@ +// Copyright 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 kmac provides function for creating KMAC instances. +// KMAC is a Message Authentication Code that based on SHA-3 and +// specified in NIST Special Publication 800-185, "SHA-3 Derived Functions: +// cSHAKE, KMAC, TupleHash and ParallelHash" [1] +// +// [1] https://doi.org/10.6028/NIST.SP.800-185 +package kmac + +import ( + "encoding/binary" + "fmt" + "hash" + + "golang.org/x/crypto/sha3" +) + +const ( + // According to [1]: + // "When used as a MAC, applications of this Recommendation shall + // not select an output length L that is less than 32 bits, and + // shall only select an output length less than 64 bits after a + // careful risk analysis is performed." + // 64 bits was selected for safety. + kmacMinimumTagSize = 8 + rate128 = 168 + rate256 = 136 +) + +// KMAC specific context +type kmac struct { + sha3.ShakeHash // cSHAKE context and Read/Write operations + tagSize int // tag size + // initBlock is the KMAC specific initialization set of bytes. It is initialized + // by newKMAC function and stores the key, encoded by the method specified in 3.3 of [1]. + // It is stored here in order for Reset() to be able to put context into + // initial state. + initBlock []byte + rate int +} + +// NewKMAC128 returns a new KMAC hash providing 128 bits of security using +// the given key, which must have 16 bytes or more, generating the given tagSize +// bytes output and using the given customizationString. +// Note that unlike other hash implementations in the standard library, +// the returned Hash does not implement encoding.BinaryMarshaler +// or encoding.BinaryUnmarshaler. +func NewKMAC128(key []byte, tagSize int, customizationString []byte) (h hash.Hash, err error) { + c := sha3.NewCShake128([]byte("KMAC"), customizationString) + h = newKMAC(key, tagSize, c, rate128) + if len(key) < 16 { + return h, fmt.Errorf("kmac: key is too short with %d bytes: should be at least %d", len(key), 16) + } + return h, nil +} + +// NewKMAC256 returns a new KMAC hash providing 256 bits of security using +// the given key, which must have 32 bytes or more, generating the given tagSize +// bytes output and using the given customizationString. +// Note that unlike other hash implementations in the standard library, +// the returned Hash does not implement encoding.BinaryMarshaler +// or encoding.BinaryUnmarshaler. +func NewKMAC256(key []byte, tagSize int, customizationString []byte) (h hash.Hash, err error) { + c := sha3.NewCShake256([]byte("KMAC"), customizationString) + h = newKMAC(key, tagSize, c, rate256) + if len(key) < 32 { + return h, fmt.Errorf("kmac: key is too short with %d bytes: should be at least %d", len(key), 32) + } + return h, nil +} + +func newKMAC(key []byte, tagSize int, c sha3.ShakeHash, rate int) hash.Hash { + if tagSize < kmacMinimumTagSize { + panic("tagSize is too small") + } + k := &kmac{ShakeHash: c, tagSize: tagSize, rate: rate} + // leftEncode returns max 9 bytes + k.initBlock = make([]byte, 0, 9+len(key)) + k.initBlock = append(k.initBlock, leftEncode(uint64(len(key)*8))...) + k.initBlock = append(k.initBlock, key...) + k.Write(bytepad(k.initBlock, k.BlockSize())) + return k +} + +// Reset resets the hash to initial state. +func (k *kmac) Reset() { + k.ShakeHash.Reset() + k.Write(bytepad(k.initBlock, k.BlockSize())) +} + +// BlockSize returns the hash block size. +func (k *kmac) BlockSize() int { + return k.rate +} + +// Size returns the tag size. +func (k *kmac) Size() int { + return k.tagSize +} + +// Sum appends the current KMAC to b and returns the resulting slice. +// It does not change the underlying hash state. +func (k *kmac) Sum(b []byte) []byte { + dup := k.ShakeHash.Clone() + dup.Write(rightEncode(uint64(k.tagSize * 8))) + hash := make([]byte, k.tagSize) + dup.Read(hash) + return append(b, hash...) +} + +func bytepad(input []byte, w int) []byte { + // leftEncode always returns max 9 bytes + buf := make([]byte, 0, 9+len(input)+w) + buf = append(buf, leftEncode(uint64(w))...) + buf = append(buf, input...) + padlen := w - (len(buf) % w) + return append(buf, make([]byte, padlen)...) +} + +func leftEncode(value uint64) []byte { + var b [9]byte + binary.BigEndian.PutUint64(b[1:], value) + // Trim all but last leading zero bytes + i := byte(1) + for i < 8 && b[i] == 0 { + i++ + } + // Prepend number of encoded bytes + b[i-1] = 9 - i + return b[i-1:] +} + +func rightEncode(value uint64) []byte { + var b [9]byte + binary.BigEndian.PutUint64(b[:8], value) + // Trim all but last leading zero bytes + i := byte(0) + for i < 7 && b[i] == 0 { + i++ + } + // Append number of encoded bytes + b[8] = 8 - i + return b[i:] +} diff --git a/internal/kmac/kmac_test.go b/internal/kmac/kmac_test.go new file mode 100644 index 000000000..07bff8c1c --- /dev/null +++ b/internal/kmac/kmac_test.go @@ -0,0 +1,142 @@ +/// Copyright 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 kmac_test implements a vector-based test suite for the cSHAKE KMAC implementation +package kmac_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "hash" + "testing" + + "github.com/ProtonMail/go-crypto/internal/kmac" +) + +// Test vectors from +// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/cSHAKE_samples.pdf +var kmacTests = []struct { + security int + key, data, customization, tag string +}{ + { + 128, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "00010203", + "", + "E5780B0D3EA6F7D3A429C5706AA43A00FADBD7D49628839E3187243F456EE14E", + }, + { + 128, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "00010203", + "My Tagged Application", + "3B1FBA963CD8B0B59E8C1A6D71888B7143651AF8BA0A7070C0979E2811324AA5", + }, + { + 128, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", + "My Tagged Application", + "1F5B4E6CCA02209E0DCB5CA635B89A15E271ECC760071DFD805FAA38F9729230", + }, + { + 256, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "00010203", + "My Tagged Application", + "20C570C31346F703C9AC36C61C03CB64C3970D0CFC787E9B79599D273A68D2F7F69D4CC3DE9D104A351689F27CF6F5951F0103F33F4F24871024D9C27773A8DD", + }, + { + 256, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", + "", + "75358CF39E41494E949707927CEE0AF20A3FF553904C86B08F21CC414BCFD691589D27CF5E15369CBBFF8B9A4C2EB17800855D0235FF635DA82533EC6B759B69", + }, + { + 256, + "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", + "My Tagged Application", + "B58618F71F92E1D56C1B8C55DDD7CD188B97B4CA4D99831EB2699A837DA2E4D970FBACFDE50033AEA585F1A2708510C32D07880801BD182898FE476876FC8965", + }, +} + +func TestKMAC(t *testing.T) { + for i, test := range kmacTests { + key, err := hex.DecodeString(test.key) + if err != nil { + t.Errorf("error decoding KAT: %s", err) + } + tag, err := hex.DecodeString(test.tag) + if err != nil { + t.Errorf("error decoding KAT: %s", err) + } + var mac hash.Hash + if test.security == 128 { + mac, err = kmac.NewKMAC128(key, len(tag), []byte(test.customization)) + } else { + mac, err = kmac.NewKMAC256(key, len(tag), []byte(test.customization)) + } + if err != nil { + t.Fatal(err) + } + data, err := hex.DecodeString(test.data) + if err != nil { + t.Errorf("error decoding KAT: %s", err) + } + mac.Write(data) + computedTag := mac.Sum(nil) + if !bytes.Equal(tag, computedTag) { + t.Errorf("#%d: got %x, want %x", i, tag, computedTag) + } + if mac.Size() != len(tag) { + t.Errorf("#%d: Size() = %x, want %x", i, mac.Size(), len(tag)) + } + // Test if it works after Reset. + mac.Reset() + mac.Write(data) + computedTag = mac.Sum(nil) + if !bytes.Equal(tag, computedTag) { + t.Errorf("#%d: got %x, want %x", i, tag, computedTag) + } + // Test if Sum does not change state. + if len(data) > 1 { + mac.Reset() + mac.Write(data[0:1]) + mac.Sum(nil) + mac.Write(data[1:]) + computedTag = mac.Sum(nil) + if !bytes.Equal(tag, computedTag) { + t.Errorf("#%d: got %x, want %x", i, tag, computedTag) + } + } + } +} +func ExampleNewKMAC256() { + key := []byte("this is a secret key; you should generate a strong random key that's at least 32 bytes long") + tag := make([]byte, 16) + msg := []byte("The quick brown fox jumps over the lazy dog") + // Example 1: Simple KMAC + k, err := kmac.NewKMAC256(key, len(tag), []byte("Partition1")) + if err != nil { + panic(err) + } + k.Write(msg) + k.Sum(tag[:0]) + fmt.Println(hex.EncodeToString(tag)) + // Example 2: Different customization string produces different digest + k, err = kmac.NewKMAC256(key, 16, []byte("Partition2")) + if err != nil { + panic(err) + } + k.Write(msg) + k.Sum(tag[:0]) + fmt.Println(hex.EncodeToString(tag)) + // Output: + //3814d78758add078334b8ab9e5c4f942 + //3762371e99e1e01ab17742b95c0360da +} diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go new file mode 100644 index 000000000..c05937654 --- /dev/null +++ b/openpgp/benchmark_v6_test.go @@ -0,0 +1,295 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + "io/ioutil" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const benchmarkMessageSize = 1024 // Signed / encrypted message size in bytes + +var benchmarkTestSet = map[string]*packet.Config{ + "RSA_1024": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 1024, + }, + "RSA_2048": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 2048, + }, + "RSA_3072": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 3072, + }, + "RSA_4096": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 4096, + }, + "Ed25519_X25519": { + Algorithm: packet.PubKeyAlgoEd25519, + }, + "Ed448_X448": { + Algorithm: packet.PubKeyAlgoEd448, + }, + "P256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP256, + }, + "P384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP384, + }, + "P521": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP521, + }, + "Brainpool256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP256, + }, + "Brainpool384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP384, + }, + "Brainpool512": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP512, + }, + "ML-DSA3Ed25519_ML-KEM768X25519": { + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + }, + "ML-DSA5Ed448_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + }, +} + +func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { + var serializedEntities [][]byte + config.V6Keys = true + + config.AEADConfig = &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + } + + config.Time = func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + b.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + b.Fatalf("Failed to serialize entity: %s", err) + } + + serializedEntities = append(serializedEntities, serializedEntity.Bytes()) + } + + return serializedEntities +} + +func benchmarkParse(b *testing.B, keys [][]byte) []*Entity { + var parsedKeys []*Entity + + b.ResetTimer() + for n := 0; n < b.N; n++ { + keyring, err := ReadKeyRing(bytes.NewReader(keys[n])) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + parsedKeys = append(parsedKeys, keyring[0]) + } + + return parsedKeys +} + +func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) [][]byte { + var encryptedMessages [][]byte + + var config = &packet.Config{ + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + V6Keys: true, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + var signed *Entity + if sign { + signed = keys[n%len(keys)] + } + + w, err := Encrypt(buf, EntityList{keys[n%len(keys)]}, signed, nil, config) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + _, err = w.Write(plaintext) + if err != nil { + b.Errorf("Error writing plaintext: %s", err) + continue + } + + err = w.Close() + if err != nil { + b.Errorf("Error closing WriteCloser: %s", err) + continue + } + + encryptedMessages = append(encryptedMessages, buf.Bytes()) + } + + return encryptedMessages +} + +func benchmarkDecrypt(b *testing.B, keys []*Entity, plaintext []byte, encryptedMessages [][]byte, verify bool) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + reader := bytes.NewReader(encryptedMessages[n%len(encryptedMessages)]) + md, err := ReadMessage(reader, EntityList{keys[n%len(keys)]}, nil, nil) + if err != nil { + b.Errorf("Error reading message: %s", err) + continue + } + + decrypted, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + b.Errorf("Error reading encrypted content: %s", err) + continue + } + + if !bytes.Equal(decrypted, plaintext) { + b.Error("Decrypted wrong plaintext") + } + + if verify { + if md.SignatureError != nil { + b.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + b.Error("Signature missing") + } + } + } +} + +func benchmarkSign(b *testing.B, keys []*Entity, plaintext []byte) [][]byte { + var signatures [][]byte + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + err := DetachSign(buf, keys[n%len(keys)], bytes.NewReader(plaintext), nil) + if err != nil { + b.Errorf("Failed to sign: %s", err) + continue + } + + signatures = append(signatures, buf.Bytes()) + } + + return signatures +} + +func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures [][]byte) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + signed := bytes.NewReader(plaintext) + signature := bytes.NewReader(signatures[n%len(signatures)]) + + parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n%len(keys)]}, signed, signature, nil) + + if signatureError != nil { + b.Errorf("Signature error: %s", signatureError) + } + + if parsedSignature == nil { + b.Error("Signature missing") + } + + if signer == nil { + b.Error("Signer missing") + } + } +} + +func BenchmarkV6Keys(b *testing.B) { + serializedKeys := make(map[string][][]byte) + parsedKeys := make(map[string][]*Entity) + encryptedMessages := make(map[string][][]byte) + encryptedSignedMessages := make(map[string][][]byte) + signatures := make(map[string][][]byte) + + var plaintext [benchmarkMessageSize]byte + _, _ = rand.Read(plaintext[:]) + + for name, config := range benchmarkTestSet { + b.Run("Generate "+name, func(b *testing.B) { + serializedKeys[name] = benchmarkGenerateKey(b, config) + b.Logf("Generate %s: %d bytes", name, len(serializedKeys[name][0])) + }) + } + + for name, keys := range serializedKeys { + b.Run("Parse_"+name, func(b *testing.B) { + parsedKeys[name] = benchmarkParse(b, keys) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_"+name, func(b *testing.B) { + encryptedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], false) + b.Logf("Encrypt %s: %d bytes", name, len(encryptedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedMessages[name], false) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_Sign_"+name, func(b *testing.B) { + encryptedSignedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], true) + b.Logf("Encrypt_Sign %s: %d bytes", name, len(encryptedSignedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_Verify_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedSignedMessages[name], true) + }) + } + + for name, keys := range parsedKeys { + b.Run("Sign_"+name, func(b *testing.B) { + signatures[name] = benchmarkSign(b, keys, plaintext[:]) + b.Logf("Sign %s: %d bytes", name, len(signatures[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Verify_"+name, func(b *testing.B) { + benchmarkVerify(b, keys, plaintext[:], signatures[name]) + }) + } +} diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index aea7f95b6..44a95a1f0 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -206,7 +206,7 @@ func Decode(data []byte) (b *Block, rest []byte) { type dashEscaper struct { buffered *bufio.Writer hashers []hash.Hash // one per key in privateKeys - hashType crypto.Hash + hashType []crypto.Hash toHash io.Writer // writes to all the hashes in hashers salts [][]byte // salts for the signatures if v6 armorHeader map[string]string // Armor headers @@ -328,7 +328,7 @@ func (d *dashEscaper) Close() (err error) { sig.Version = k.Version sig.SigType = packet.SigTypeText sig.PubKeyAlgo = k.PubKeyAlgo - sig.Hash = d.hashType + sig.Hash = d.hashType[i] sig.CreationTime = t sig.IssuerKeyId = &k.KeyId sig.IssuerFingerprint = k.Fingerprint @@ -399,14 +399,16 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config return nil, errors.UnsupportedError("unsupported hash type: " + strconv.Itoa(int(hashType))) } var hashers []hash.Hash + var hashTypes []crypto.Hash var ws []io.Writer var salts [][]byte for _, sk := range privateKeys { - h := hashType.New() + selectedHash := sk.PubKeyAlgo.HandleSpecificHash(hashType) + h := selectedHash.New() if sk.Version == 6 { // generate salt var salt []byte - salt, err = packet.SignatureSaltForHash(hashType, config.Random()) + salt, err = packet.SignatureSaltForHash(selectedHash, config.Random()) if err != nil { return } @@ -416,6 +418,7 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config salts = append(salts, salt) } hashers = append(hashers, h) + hashTypes = append(hashTypes, selectedHash) ws = append(ws, h) } toHash := io.MultiWriter(ws...) @@ -446,7 +449,7 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config plaintext = &dashEscaper{ buffered: buffered, hashers: hashers, - hashType: hashType, + hashType: hashTypes, toHash: toHash, salts: salts, armorHeader: headers, diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index 0c3c49c31..ef9c18bff 100644 --- a/openpgp/integration_tests/v2/utils_test.go +++ b/openpgp/integration_tests/v2/utils_test.go @@ -30,10 +30,11 @@ func generateFreshTestVectors(num int) (vectors []testVector, err error) { v = "v6" } pkAlgoNames := map[packet.PublicKeyAlgorithm]string{ - packet.PubKeyAlgoRSA: "rsa_" + v, - packet.PubKeyAlgoEdDSA: "EdDSA_" + v, - packet.PubKeyAlgoEd25519: "ed25519_" + v, - packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v, } newVector := testVector{ @@ -238,6 +239,7 @@ func randConfig() *packet.Config { packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoEd25519, packet.PubKeyAlgoEd448, + packet.PubKeyAlgoMldsa65Ed25519, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -268,7 +270,9 @@ func randConfig() *packet.Config { compConf := &packet.CompressionConfig{Level: level} var v6 bool - if mathrand.Int()%2 == 0 { + if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 { + v6 = true + } else if mathrand.Int()%2 == 0 { v6 = true if pkAlgo == packet.PubKeyAlgoEdDSA { pkAlgo = packet.PubKeyAlgoEd25519 diff --git a/openpgp/internal/ecc/curves.go b/openpgp/internal/ecc/curves.go index 5ed9c93b3..34c4ad860 100644 --- a/openpgp/internal/ecc/curves.go +++ b/openpgp/internal/ecc/curves.go @@ -16,6 +16,8 @@ type ECDSACurve interface { UnmarshalIntegerPoint([]byte) (x, y *big.Int) MarshalIntegerSecret(d *big.Int) []byte UnmarshalIntegerSecret(d []byte) *big.Int + MarshalFieldInteger(d *big.Int) []byte + UnmarshalFieldInteger(d []byte) *big.Int GenerateECDSA(rand io.Reader) (x, y, secret *big.Int, err error) Sign(rand io.Reader, x, y, d *big.Int, hash []byte) (r, s *big.Int, err error) Verify(x, y *big.Int, hash []byte, r, s *big.Int) bool diff --git a/openpgp/internal/ecc/generic.go b/openpgp/internal/ecc/generic.go index e28d7c710..44fad3b49 100644 --- a/openpgp/internal/ecc/generic.go +++ b/openpgp/internal/ecc/generic.go @@ -56,6 +56,15 @@ func (c *genericCurve) UnmarshalIntegerSecret(d []byte) *big.Int { return new(big.Int).SetBytes(d) } +func (c *genericCurve) MarshalFieldInteger(i *big.Int) (b []byte) { + b = make([]byte, (c.Curve.Params().BitSize+7)/8) + return i.FillBytes(b) +} + +func (c *genericCurve) UnmarshalFieldInteger(d []byte) *big.Int { + return new(big.Int).SetBytes(d) +} + func (c *genericCurve) GenerateECDH(rand io.Reader) (point, secret []byte, err error) { secret, x, y, err := elliptic.GenerateKey(c.Curve, rand) if err != nil { diff --git a/openpgp/internal/encoding/octetarray.go b/openpgp/internal/encoding/octetarray.go new file mode 100644 index 000000000..e5e4a8274 --- /dev/null +++ b/openpgp/internal/encoding/octetarray.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package encoding + +import ( + "io" +) + +// OctetArray is used to store a fixed-length field +type OctetArray struct { + length int + bytes []byte +} + +// NewOctetArray returns a OID initialized with bytes. +func NewOctetArray(bytes []byte) *OctetArray { + return &OctetArray{ + length: len(bytes), + bytes: bytes, + } +} + +func NewEmptyOctetArray(length int) *OctetArray { + return &OctetArray{ + length: length, + bytes: nil, + } +} + +// Bytes returns the decoded data. +func (o *OctetArray) Bytes() []byte { + return o.bytes +} + +// BitLength is the size in bits of the decoded data. +func (o *OctetArray) BitLength() uint16 { + return uint16(o.length * 8) +} + +// EncodedBytes returns the encoded data. +func (o *OctetArray) EncodedBytes() []byte { + if len(o.bytes) != o.length { + panic("invalid length") + } + return o.bytes +} + +// EncodedLength is the size in bytes of the encoded data. +func (o *OctetArray) EncodedLength() uint16 { + return uint16(o.length) +} + +// ReadFrom reads into b the next OID from r. +func (o *OctetArray) ReadFrom(r io.Reader) (int64, error) { + o.bytes = make([]byte, o.length) + + nn, err := io.ReadFull(r, o.bytes) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + return int64(nn), err +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index b3c5c5daa..df4fab454 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -21,6 +21,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" @@ -323,6 +325,21 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { case packet.ExperimentalPubKeyAlgoHMAC: hash := algorithm.HashById[hashToHashId(config.Hash())] return symmetric.HMACGenerateKey(config.Random(), hash) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -330,7 +347,8 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // Generates an encryption/decryption key func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { - switch config.PublicKeyAlgorithm() { + pubKeyAlgo := config.PublicKeyAlgorithm() + switch pubKeyAlgo { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() if bits < 1024 { @@ -368,6 +386,26 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.ExperimentalPubKeyAlgoAEAD: cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem_x25519 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/keys.go b/openpgp/keys.go index 284a941cb..da3809df3 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -134,6 +134,7 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 + isPQ := false var maxTime time.Time for i, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && @@ -142,9 +143,10 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { !subkey.PublicKey.KeyExpired(subkey.Sig, now) && !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && - (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) { + (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime) || (!isPQ && subkey.IsPQ())) { candidateSubkey = i maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -201,6 +203,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 var maxTime time.Time + isPQ := false for idx, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && (flags&packet.KeyFlagCertify == 0 || subkey.Sig.FlagCertify) && @@ -210,9 +213,11 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() } } @@ -305,6 +310,11 @@ func (s *Subkey) Revoked(now time.Time) bool { return revoked(s.Revocations, now) } +// IsPQ returns true if the algorithm is Post-Quantum safe. +func (s *Subkey) IsPQ() bool { + return s.PublicKey.IsPQ() +} + // Revoked returns whether the key or subkey has been revoked by a self-signature. // Note that third-party revocation signatures are not supported. // Note also that Identity revocation should be checked separately. diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 184325a61..8bddbeb7a 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2085,3 +2085,68 @@ TxGVotQ4A/0u0VbOMEUfnrI8Fms= t.Errorf("Expected AEAD subkey") } } + +func testAddMlkemSubkey(t *testing.T, entity *Entity, v6Keys bool) { + var err error + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + for name, algo := range asymmAlgos { + // Remove existing subkeys + entity.Subkeys = []Subkey{} + + t.Run(name, func(t *testing.T) { + kyberConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: v6Keys, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + err = entity.AddEncryptionSubkey(kyberConfig) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if entity.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + + if entity.Subkeys[0].PublicKey.Version != entity.PrivateKey.Version { + t.Fatalf("Expected subkey version: %d, got: %d", entity.PrivateKey.Version, + entity.Subkeys[0].PublicKey.Version) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(read.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if read.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + }) + } +} diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index fc9ba776d..7914d3eb0 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -3,8 +3,14 @@ package openpgp import ( "bytes" "crypto" + "crypto/rand" "strings" "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -196,3 +202,138 @@ func TestNewEntityWithDefaultHashv6(t *testing.T) { } } } + +func TestGeneratePqKey(t *testing.T) { + randomPassword := make([]byte, 128) + _, err := rand.Read(randomPassword) + if err != nil { + t.Fatal(err) + } + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for name, algo := range asymmAlgos { + t.Run(name, func(t *testing.T) { + config := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatalf("Failed to parse entity: %s", err) + } + + if read.PrimaryKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", algo, read.PrimaryKey.PubKeyAlgo) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err := read.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-DSA key was marked as invalid: ", err) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { + bin, err := pk.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + if pk.PublicMldsa, err = pk.Mldsa.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal(err) + } + } + + err = read.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-DSA key") + } + + testMlkemSubkey(t, read.Subkeys[0], randomPassword) + }) + } +} + +func testMlkemSubkey(t *testing.T, subkey Subkey, randomPassword []byte) { + var err error + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err = subkey.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-KEM key was marked as invalid: ", err) + } + + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + // Corrupt public ML-KEM in primary key + if pk, ok := subkey.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey); ok { + bin, _ := pk.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + if pk.PublicMlkem, err = pk.Mlkem.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal("unable to corrupt key") + } + } else { + t.Fatal("Invalid subkey") + } + + err = subkey.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-KEM key") + } +} + +func TestAddV6MlkemSubkey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + testAddMlkemSubkey(t, entity, true) +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go new file mode 100644 index 000000000..82cdf199c --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -0,0 +1,105 @@ +// Package mldsa_eddsa implements hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-composite-signature-schemes +package mldsa_eddsa + +import ( + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/sign" +) + +const ( + MlDsaSeedLen = 32 +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.EdDSACurve + Mldsa sign.Scheme + PublicPoint []byte + PublicMldsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMldsa sign.PrivateKey + SecretMldsaSeed []byte +} + +// GenerateKey generates a ML-DSA + EdDSA composite key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-generation-procedure-2 +func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mldsa = d + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateEdDSA(rand) + if err != nil { + return nil, err + } + + keySeed := make([]byte, d.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + + if err := priv.DeriveMlDsaKeys(keySeed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlDsaKeys derives the ML-DSA keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlDsaKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlDsaSeedLen { + return goerrors.New("mldsa_eddsa: ml-dsa secret seed has the wrong length") + } + priv.SecretMldsaSeed = seed + publicKey, privateKey := priv.PublicKey.Mldsa.DeriveKey(priv.SecretMldsaSeed) + if overridePublicKey { + priv.PublicKey.PublicMldsa = publicKey + } + priv.SecretMldsa = privateKey + return nil +} + +// Sign generates a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-generation +func Sign(priv *PrivateKey, message []byte) (dSig, ecSig []byte, err error) { + ecSig, err = priv.PublicKey.Curve.Sign(priv.PublicKey.PublicPoint, priv.SecretEc, message) + if err != nil { + return nil, nil, err + } + + dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message, nil) + if dSig == nil { + return nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") + } + + return dSig, ecSig, nil +} + +// Verify verifies a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-verification +func Verify(pub *PublicKey, message, dSig, ecSig []byte) bool { + return pub.Curve.Verify(pub.PublicPoint, message, ecSig) && pub.Mldsa.Verify(pub.PublicMldsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateEdDSA(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicMldsa.Equal(priv.SecretMldsa.Public()) { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + + return nil +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go new file mode 100644 index 000000000..20b9e1604 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -0,0 +1,95 @@ +// Package mldsa_eddsa_test tests the implementation of hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +package mldsa_eddsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA3_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicMldsa, err = key.Mldsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_eddsa.PrivateKey { + curveObj, err := packet.GetEdDSACurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMldsaFromAlgID(algId) + if err != nil { + t.Errorf("error getting ML-DSA: %s", err) + } + + priv, err := mldsa_eddsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *mldsa_eddsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, ecSig, err := mldsa_eddsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := mldsa_eddsa.Verify(&priv.PublicKey, digest, dSig, ecSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go new file mode 100644 index 000000000..686a25528 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -0,0 +1,286 @@ +// Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +// It follows the spec https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-composite-kem-schemes +package mlkem_ecdh + +import ( + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/internal/kmac" + "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "golang.org/x/crypto/sha3" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/kem" +) + +const ( + maxSessionKeyLength = 64 + domainSeparator = "OpenPGPCompositeKDFv1" + MlKemSeedLen = 64 +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.ECDHCurve + Mlkem kem.Scheme + PublicMlkem kem.PublicKey + PublicPoint []byte +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMlkem kem.PrivateKey + SecretMlkemSeed []byte +} + +// GenerateKey implements ML-KEM + ECC key generation as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-generation-procedure +func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mlkem = k + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateECDH(rand) + if err != nil { + return nil, err + } + + seed, err := generateRandomSeed(rand, MlKemSeedLen) + if err != nil { + return nil, err + } + + if err := priv.DeriveMlKemKeys(seed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlKemKeys derives the ML-KEM keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlKemKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlKemSeedLen { + return goerrors.New("mlkem_ecdh: ml-kem secret seed has the wrong length") + } + priv.SecretMlkemSeed = seed + publicKey, privateKey := priv.PublicKey.Mlkem.DeriveKeyPair(priv.SecretMlkemSeed) + if overridePublicKey { + priv.PublicKey.PublicMlkem = publicKey + } + priv.SecretMlkem = privateKey + return nil +} + +// Encrypt implements ML-KEM + ECC encryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-encryption-procedure +func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { + if len(msg) > maxSessionKeyLength { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") + } + + if len(msg)%8 != 0 { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key not a multiple of 8") + } + + // EC shared secret derivation + ecEphemeral, ecSS, err := pub.Curve.Encaps(rand, pub.PublicPoint) + if err != nil { + return nil, nil, nil, err + } + + // ML-KEM shared secret derivation + kyberSeed, err := generateRandomSeed(rand, pub.Mlkem.EncapsulationSeedSize()) + if err != nil { + return nil, nil, nil, err + } + + kEphemeral, kSS, err := pub.Mlkem.EncapsulateDeterministically(pub.PublicMlkem, kyberSeed) + if err != nil { + return nil, nil, nil, err + } + + keyEncryptionKey, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS, kEphemeral, pub.PublicMlkem) + if err != nil { + return nil, nil, nil, err + } + + if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { + return nil, nil, nil, err + } + + return kEphemeral, ecEphemeral, ciphertext, nil +} + +// Decrypt implements ML-KEM + ECC decryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-decryption-procedure +func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg []byte, err error) { + // EC shared secret derivation + ecSS, err := priv.PublicKey.Curve.Decaps(ecEphemeral, priv.SecretEc) + if err != nil { + return nil, err + } + + // ML-KEM shared secret derivation + kSS, err := priv.PublicKey.Mlkem.Decapsulate(priv.SecretMlkem, kEphemeral) + if err != nil { + return nil, err + } + + kek, err := buildKey(&priv.PublicKey, ecSS, ecEphemeral, priv.PublicPoint, kSS, kEphemeral, priv.PublicMlkem) + if err != nil { + return nil, err + } + + return keywrap.Unwrap(kek, ciphertext) +} + +// buildKey implements the composite KDF as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-combiner +func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { + h := sha3.New256() + + // SHA3 never returns error + _, _ = h.Write(eccSecretPoint) + _, _ = h.Write(eccEphemeral) + _, _ = h.Write(eccPublicKey) + eccKeyShare := h.Sum(nil) + + serializedMlkemPublicKey, err := mlkemPublicKey.MarshalBinary() + if err != nil { + return nil, err + } + + // mlkemKeyShare - the ML-KEM key share encoded as an octet string + // mlkemEphemeral - the ML-KEM ciphertext encoded as an octet string + // mlkemPublicKey - The ML-KEM public key of the recipient as an octet string + // algId - the OpenPGP algorithm ID of the public-key encryption algorithm + // domainSeparator – the UTF-8 encoding of the string "OpenPGPCompositeKDFv1" + // eccKeyShare - the ECDH key share encoded as an octet string + // eccEphemeral - the ECDH ciphertext encoded as an octet string + // eccPublicKey - The ECDH public key of the recipient as an octet string + + // KEK = KMAC256( + // mlkemKeyShare || eccKeyShare, + // mlkemEphemeral || eccEphemeral || mlkemPublicKey || ecdhPublicKey || algId, + // 256 (32 bytes), + // domainSeparator + // ) + + kMacKeyBuffer := make([]byte, len(mlkemKeyShare)+len(eccKeyShare)) + copy(kMacKeyBuffer[:len(mlkemKeyShare)], mlkemKeyShare) + copy(kMacKeyBuffer[len(mlkemKeyShare):], eccKeyShare) + + k, err := kmac.NewKMAC256(kMacKeyBuffer, 32, []byte(domainSeparator)) + if err != nil { + return nil, err + } + + // kmac hash never returns an error + _, _ = k.Write(mlkemEphemeral) + _, _ = k.Write(eccEphemeral) + _, _ = k.Write(serializedMlkemPublicKey) + _, _ = k.Write(eccPublicKey) + _, _ = k.Write([]byte{pub.AlgId}) + + return k.Sum(nil), nil +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateECDH(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicKey.PublicMlkem.Equal(priv.SecretMlkem.Public()) { + return errors.KeyInvalidError("mlkem_ecdh: invalid public key") + } + + return +} + +// EncodeFields encodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(w io.Writer, ec, ml, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + if _, err = w.Write(ec); err != nil { + return err + } + + if _, err = w.Write(ml); err != nil { + return err + } + + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + + if _, err = w.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + + if !v6 { + if _, err = w.Write([]byte{cipherFunction}); err != nil { + return err + } + } + + if _, err = w.Write(encryptedSessionKey); err != nil { + return err + } + + return nil +} + +// DecodeFields decodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, encryptedMPI2, encryptedMPI3 encoding.Field, cipherFunction byte, err error) { + var buf [1]byte + + encryptedMPI1 = encoding.NewEmptyOctetArray(lenEcc) + if _, err = encryptedMPI1.ReadFrom(r); err != nil { + return + } + + encryptedMPI2 = encoding.NewEmptyOctetArray(lenMlkem) + if _, err = encryptedMPI2.ReadFrom(r); err != nil { + return + } + + // A one-octet size of the following fields. + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + cipherFunction = buf[0] + followingLen -= 1 + } + + // The encrypted session key. + encryptedMPI3 = encoding.NewEmptyOctetArray(int(followingLen)) + if _, err = encryptedMPI3.ReadFrom(r); err != nil { + return + } + + return +} + +func generateRandomSeed(rand io.Reader, size int) ([]byte, error) { + randomBytes := make([]byte, size) + if _, err := rand.Read(randomBytes); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + return randomBytes, nil +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go new file mode 100644 index 000000000..23a82eb36 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -0,0 +1,104 @@ +// Package mlkem_ecdh_test tests the implementation of hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +package mlkem_ecdh_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestEncryptDecrypt(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + symmAlgos := map[string]algorithm.Cipher{ + "AES-128": algorithm.AES128, + "AES-192": algorithm.AES192, + "AES-256": algorithm.AES256, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + for symmName, symmAlgo := range symmAlgos { + t.Run(symmName, func(t *testing.T) { + testEncryptDecryptAlgo(t, key, symmAlgo) + }) + } + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + var err error + key := testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, _ := key.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + key.PublicMlkem, err = key.Mlkem.UnmarshalBinaryPublicKey(bin) + if err != nil { + t.Fatal("unable to corrupt key") + } + + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_ecdh.PrivateKey { + curveObj, err := packet.GetECDHCurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMlkemFromAlgID(algId) + if err != nil { + t.Errorf("error getting kyber: %s", err) + } + + priv, err := mlkem_ecdh.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testEncryptDecryptAlgo(t *testing.T, priv *mlkem_ecdh.PrivateKey, kdfCipher algorithm.Cipher) { + expectedMessage := make([]byte, kdfCipher.KeySize()) // encryption algo + checksum + rand.Read(expectedMessage) + + kE, ecE, c, err := mlkem_ecdh.Encrypt(rand.Reader, &priv.PublicKey, expectedMessage) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + decryptedMessage, err := mlkem_ecdh.Decrypt(priv, kE, ecE, c) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + if !bytes.Equal(decryptedMessage, expectedMessage) { + t.Errorf("decryption failed, got: %x, want: %x", decryptedMessage, expectedMessage) + } +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 6a2dbd50b..bdc33812b 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -19,6 +19,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -35,10 +36,12 @@ type EncryptedKey struct { CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt - encryptedMPI1, encryptedMPI2 encoding.Field - ephemeralPublicX25519 *x25519.PublicKey // used for x25519 - ephemeralPublicX448 *x448.PublicKey // used for x448 - encryptedSession []byte // used for x25519 and x448 + encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, and PQC keys + encryptedMPI2 encoding.Field // Only valid in Elgamal, ECDH and PQC keys + encryptedMPI3 encoding.Field // Only valid in PQC keys + ephemeralPublicX25519 *x25519.PublicKey // used for x25519 + ephemeralPublicX448 *x448.PublicKey // used for x448 + encryptedSession []byte // used for x25519 and x448 nonce []byte aeadMode algorithm.AEADMode @@ -153,12 +156,20 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { return } + case PubKeyAlgoMlkem768X25519: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 32, 1088, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem1024X448: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 56, 1568, e.Version == 6); err != nil { + return err + } } if e.Version < 6 { switch e.Algo { - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: e.CipherFunc = CipherFunction(cipherFunction) - // Check for validiy is in the Decrypt method + // Check for validity is in the Decrypt method } } @@ -214,6 +225,12 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { case ExperimentalPubKeyAlgoAEAD: priv := priv.PrivateKey.(*symmetric.AEADPrivateKey) b, err = priv.Decrypt(e.nonce, e.encryptedMPI1.Bytes(), e.aeadMode) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + ecE := e.encryptedMPI1.Bytes() + kE := e.encryptedMPI2.Bytes() + m := e.encryptedMPI3.Bytes() + + b, err = mlkem_ecdh.Decrypt(priv.PrivateKey.(*mlkem_ecdh.PrivateKey), kE, ecE, m) default: err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } @@ -233,22 +250,22 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } } key, err = decodeChecksumKey(b[keyOffset:]) - if err != nil { - return err - } - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: if e.Version < 6 { switch e.CipherFunc { case CipherAES128, CipherAES192, CipherAES256: break default: - return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448") + return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519, x448, and PQC") } } key = b[:] default: return errors.UnsupportedError("unsupported algorithm for decryption") } + if err != nil { + return err + } e.Key = key return nil } @@ -267,6 +284,11 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6) case PubKeyAlgoX448: encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + int(e.encryptedMPI3.EncodedLength()) + 1 + if e.Version < 6 { + encodedLength += 1 + } default: return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo))) } @@ -337,6 +359,9 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { case PubKeyAlgoX448: err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6) return err + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + err := mlkem_ecdh.EncodeFields(w, e.encryptedMPI1.EncodedBytes(), e.encryptedMPI2.EncodedBytes(), e.encryptedMPI3.EncodedBytes(), byte(e.CipherFunc), e.Version == 6) + return err default: panic("internal error") } @@ -369,13 +394,13 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal { return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed") } - // In v3 PKESKs, for x25519 and x448, mandate using AES - if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) { - switch cipherFunc { - case CipherAES128, CipherAES192, CipherAES256: - break + // In v3 PKESKs, for X25519 and X448, mandate using AES + if version == 3 && cipherFunc != CipherAES128 && cipherFunc != CipherAES192 && cipherFunc != CipherAES256 { + switch pub.PubKeyAlgo { + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519, x448, and PQC") default: - return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448") + break } } @@ -424,7 +449,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: // algorithm is added in plaintext below keyBlock = key } @@ -442,6 +467,8 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) case ExperimentalPubKeyAlgoAEAD: return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return serializeEncryptedKeyMlkem(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*mlkem_ecdh.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly, ExperimentalPubKeyAlgoHMAC: return errors.InvalidArgumentError("cannot encrypt to public key of type " + strconv.Itoa(int(pub.PubKeyAlgo))) } @@ -636,3 +663,32 @@ func encodeChecksumKey(buffer []byte, key []byte) { buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } + +func serializeEncryptedKeyMlkem(w io.Writer, rand io.Reader, header []byte, pub *mlkem_ecdh.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + mlE, ecE, c, err := mlkem_ecdh.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("ML-KEM + ECDH encryption failed: " + err.Error()) + } + + ml := encoding.NewOctetArray(mlE) + ec := encoding.NewOctetArray(ecE) + m := encoding.NewOctetArray(c) + + packetLen := len(header) /* header length */ + packetLen += int(ec.EncodedLength()) + int(ml.EncodedLength()) + int(m.EncodedLength()) + 1 + if version < 6 { + packetLen += 1 + } + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header) + if err != nil { + return err + } + + return mlkem_ecdh.EncodeFields(w, ec.EncodedBytes(), ml.EncodedBytes(), m.EncodedBytes(), cipherFunc, version == 6) +} diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index dd4ad34c6..f65efb2e5 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -8,6 +8,7 @@ package packet // import "github.com/ProtonMail/go-crypto/openpgp/packet" import ( "bytes" + "crypto" "crypto/cipher" "crypto/rsa" "io" @@ -512,13 +513,22 @@ const ( // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 PubKeyAlgoRSASignOnly PublicKeyAlgorithm = 3 + + // Experimental PQC KEM algorithms + PubKeyAlgoMlkem768X25519 = 105 + PubKeyAlgoMlkem1024X448 = 106 + + // Experimental PQC DSA algorithms + PubKeyAlgoMldsa65Ed25519 = 107 + PubKeyAlgoMldsa87Ed448 = 108 ) // CanEncrypt returns true if it's possible to encrypt a message to a public // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, ExperimentalPubKeyAlgoAEAD: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, ExperimentalPubKeyAlgoAEAD, + PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return true } return false @@ -528,12 +538,25 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { // sign a message. func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: return true } return false } +// HandleSpecificHash returns the mandated hash if the algorithm requires it; +// otherwise, it returns the selectedHash. +func (pka PublicKeyAlgorithm) HandleSpecificHash(selectedHash crypto.Hash) crypto.Hash { + switch pka { + case PubKeyAlgoMldsa65Ed25519: + return crypto.SHA3_256 + case PubKeyAlgoMldsa87Ed448: + return crypto.SHA3_512 + } + return selectedHash +} + // CipherFunction represents the different block ciphers specified for OpenPGP. See // http://www.iana.org/assignments/pgp-parameters/pgp-parameters.xhtml#pgp-parameters-13 type CipherFunction algorithm.CipherFunction diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 406c56e65..aa0bfd61d 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -13,6 +13,7 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" + goerrors "errors" "fmt" "io" "math/big" @@ -27,6 +28,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/s2k" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" @@ -169,6 +172,8 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) case *symmetric.HMACPrivateKey: pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) + case *mldsa_eddsa.PrivateKey: + pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -176,7 +181,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey return pk } -// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey. +// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448|mlkem_ecdh}.PrivateKey. func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey { pk := new(PrivateKey) switch priv := decrypter.(type) { @@ -192,6 +197,8 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) case *symmetric.AEADPrivateKey: pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) + case *mlkem_ecdh.PrivateKey: + pk.PublicKey = *NewMlkemEcdhPublicKey(creationTime, &priv.PublicKey) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -550,7 +557,29 @@ func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err e return } _, err = w.Write(priv.Key) - return + return err +} + +// serializeMlkemPrivateKey serializes a ML-KEM + ECC private key according to +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets +func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err error) { + if _, err = w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + _, err = w.Write(encoding.NewOctetArray(priv.SecretMlkemSeed).EncodedBytes()) + return err +} + +// serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 +func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) error { + if _, err := w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + if _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsaSeed).EncodedBytes()); err != nil { + return err + } + return nil } // decrypt decrypts an encrypted private key using a decryption key. @@ -857,6 +886,10 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeAEADPrivateKey(w, priv) case *symmetric.HMACPrivateKey: err = serializeHMACPrivateKey(w, priv) + case *mlkem_ecdh.PrivateKey: + err = serializeMlkemPrivateKey(w, priv) + case *mldsa_eddsa.PrivateKey: + err = serializeMldsaEddsaPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -885,13 +918,21 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseEd25519PrivateKey(data) case PubKeyAlgoEd448: return pk.parseEd448PrivateKey(data) - default: - err = errors.StructuralError("unknown private key type") - return case ExperimentalPubKeyAlgoAEAD: return pk.parseAEADPrivateKey(data) case ExperimentalPubKeyAlgoHMAC: return pk.parseHMACPrivateKey(data) + case PubKeyAlgoMlkem768X25519: + return pk.parseMlkemEcdhPrivateKey(data, 32, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMlkem1024X448: + return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMldsa65Ed25519: + return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa_eddsa.MlDsaSeedLen) + case PubKeyAlgoMldsa87Ed448: + return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa_eddsa.MlDsaSeedLen) + default: + err = errors.StructuralError("unknown private key type") + return } } @@ -1212,6 +1253,72 @@ func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { return nil } +// parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 +func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, seedLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + EdDSA key") + } + pub := pk.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey) + priv := new(mldsa_eddsa.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlDsaKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mldsa_eddsa.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseMlkemEcdhPrivateKey parses a ML-KEM + ECC private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets +func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, seedLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-KEM + ECDH key") + } + pub := pk.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey) + priv := new(mlkem_ecdh.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlKemKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mlkem_ecdh.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + func validateDSAParameters(priv *dsa.PrivateKey) error { p := priv.P // group prime q := priv.Q // subgroup order diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f53036510..823f82d07 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,12 +5,14 @@ package packet import ( + "crypto" "crypto/dsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" _ "crypto/sha512" "encoding/binary" + goerrors "errors" "fmt" "hash" "io" @@ -28,9 +30,17 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" + "github.com/cloudflare/circl/kem" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. @@ -38,7 +48,7 @@ type PublicKey struct { Version int CreationTime time.Time PubKeyAlgo PublicKeyAlgorithm - PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey + PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey, or *mlkem_ecdh.PublicKey Fingerprint []byte KeyId uint64 IsSubkey bool @@ -232,8 +242,7 @@ func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey } func NewAEADPublicKey(creationTime time.Time, pub *symmetric.AEADPublicKey) *PublicKey { - var pk *PublicKey - pk = &PublicKey{ + pk := &PublicKey{ Version: 4, CreationTime: creationTime, PubKeyAlgo: ExperimentalPubKeyAlgoAEAD, @@ -244,8 +253,7 @@ func NewAEADPublicKey(creationTime time.Time, pub *symmetric.AEADPublicKey) *Pub } func NewHMACPublicKey(creationTime time.Time, pub *symmetric.HMACPublicKey) *PublicKey { - var pk *PublicKey - pk = &PublicKey{ + pk := &PublicKey{ Version: 4, CreationTime: creationTime, PubKeyAlgo: ExperimentalPubKeyAlgoHMAC, @@ -255,6 +263,43 @@ func NewHMACPublicKey(creationTime time.Time, pub *symmetric.HMACPublicKey) *Pub return pk } +func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *PublicKey { + mlkemBin, err := pub.PublicMlkem.MarshalBinary() + if err != nil { + panic(err) + } + + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(mlkemBin), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) *PublicKey { + publicKeyBytes, err := pub.PublicMldsa.MarshalBinary() + if err != nil { + panic(err) + } + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(publicKeyBytes), + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -283,7 +328,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { } pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0) pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5]) - // Ignore four-ocet length + // Ignore four-octet length switch pk.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: err = pk.parseRSA(r) @@ -309,6 +354,14 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseAEAD(r) case ExperimentalPubKeyAlgoHMAC: err = pk.parseHMAC(r) + case PubKeyAlgoMlkem768X25519: + err = pk.parseMlkemEcdh(r, 32, mlkem768.PublicKeySize) + case PubKeyAlgoMlkem1024X448: + err = pk.parseMlkemEcdh(r, 56, mlkem1024.PublicKeySize) + case PubKeyAlgoMldsa65Ed25519: + err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize) + case PubKeyAlgoMldsa87Ed448: + err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -525,6 +578,41 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return } +// parseMlkemEcdh parses a ML-KEM + ECC public key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets +func (pk *PublicKey) parseMlkemEcdh(r io.Reader, ecLen, kLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(kLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mlkem_ecdh.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetECDHCurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mlkem, err = GetMlkemFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMlkem, err = pub.Mlkem.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub + + return +} + func (pk *PublicKey) parseEdDSA(r io.Reader) (err error) { if pk.Version == 6 { // Implementations MUST NOT accept or generate version 6 key material using the deprecated OIDs. @@ -672,6 +760,40 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { _, err = readFull(r, bindingHash[:]) + return bindingHash, err +} + +// parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 +func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(dLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mldsa_eddsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetEdDSACurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMldsa, err = pub.Mldsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub return } @@ -765,6 +887,10 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: length += 1 // Hash octet length += 32 // Binding hash + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.q.EncodedLength()) default: panic("unknown public key algorithm") } @@ -873,13 +999,20 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(symmKey.BindingHash[:]) return + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(pk.p.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.q.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } // CanSign returns true iff this public key can generate signatures func (pk *PublicKey) CanSign() bool { - return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH + return pk.PubKeyAlgo.CanSign() } // VerifyHashTag returns nil iff sig appears to be a plausible signature of the data @@ -970,6 +1103,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("HMAC verification failure") } return nil + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if pk.PubKeyAlgo == PubKeyAlgoMldsa65Ed25519 && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: Mldsa65Ed25519 requires sha3-256 message hash: has %s", sig.Hash)) + } + if pk.PubKeyAlgo == PubKeyAlgoMldsa87Ed448 && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: Mldsa87Ed448 requires sha3-512 message hash: has %s", sig.Hash)) + } + mldsaEddsaPublicKey := pk.PublicKey.(*mldsa_eddsa.PublicKey) + if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.EdDSASigR.Bytes()) { + return errors.SignatureError("MldsaEddsa verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1193,6 +1338,9 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed448.PublicKeySize * 8 case ExperimentalPubKeyAlgoAEAD: bitLength = 32 + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1231,3 +1379,71 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second) return currentTime.Unix() > expiry.Unix() } + +// IsPQ returns true if the algorithm of this public key is Post-Quantum safe. +func (pg *PublicKey) IsPQ() bool { + switch pg.PubKeyAlgo { + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, + PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } +} + +func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return PubKeyAlgoMlkem768X25519, nil + case PubKeyAlgoMldsa87Ed448: + return PubKeyAlgoMlkem1024X448, nil + default: + return 0, goerrors.New("packet: unsupported pq public key algorithm") + } +} + +// GetMlkemFromAlgID returns the ML-KEM instance from the matching KEM +func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return mlkem768.Scheme(), nil + case PubKeyAlgoMlkem1024X448: + return mlkem1024.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-KEM public key algorithm") + } +} + +// GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM +func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return ecc.NewCurve25519(), nil + case PubKeyAlgoMlkem1024X448: + return ecc.NewX448(), nil + default: + return nil, goerrors.New("packet: unsupported ECDH public key algorithm") + } +} + +func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return ecc.NewEd25519(), nil + case PubKeyAlgoMldsa87Ed448: + return ecc.NewEd448(), nil + default: + return nil, goerrors.New("packet: unsupported EdDSA public key algorithm") + } +} + +func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return mldsa65.Scheme(), nil + case PubKeyAlgoMldsa87Ed448: + return mldsa87.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-DSA public key algorithm") + } +} diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 28ecc33a2..034f39812 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -10,6 +10,7 @@ import ( "crypto/dsa" "encoding/asn1" "encoding/binary" + "fmt" "hash" "io" "math/big" @@ -23,6 +24,9 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) const ( @@ -80,8 +84,10 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field - EdSig []byte HMAC encoding.Field + EdSig []byte + MldsaSig encoding.Field + SlhdsaSig encoding.Field // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -199,7 +205,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { sig.SigType = SignatureType(buf[0]) sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -342,12 +349,33 @@ func (sig *Signature) parse(r io.Reader) (err error) { if _, err = sig.HMAC.ReadFrom(r); err != nil { return } + case PubKeyAlgoMldsa65Ed25519: + if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { + return + } + case PubKeyAlgoMldsa87Ed448: + if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { + return + } default: panic("unreachable") } return } +// parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-packet-tag-2 +func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (err error) { + sig.EdDSASigR = encoding.NewEmptyOctetArray(ecLen) + if _, err = sig.EdDSASigR.ReadFrom(r); err != nil { + return + } + + sig.MldsaSig = encoding.NewEmptyOctetArray(dLen) + _, err = sig.MldsaSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 9580, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -1007,6 +1035,23 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e if err == nil { sig.HMAC = encoding.NewShortByteString(sigdata) } + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if sig.Version != 6 { + return errors.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + if priv.PubKeyAlgo == PubKeyAlgoMldsa65Ed25519 && sig.Hash != crypto.SHA3_256 { + return errors.StructuralError(fmt.Sprintf("Mldsa65Ed25519 requires sha3-256 message hash: got %s", sig.Hash)) + } + if priv.PubKeyAlgo == PubKeyAlgoMldsa87Ed448 && sig.Hash != crypto.SHA3_512 { + return errors.StructuralError(fmt.Sprintf("Mldsa87Ed448 requires sha3-512 message hash: got %s", sig.Hash)) + } + sk := priv.PrivateKey.(*mldsa_eddsa.PrivateKey) + dSig, ecSig, err := mldsa_eddsa.Sign(sk, digest) + + if err == nil { + sig.MldsaSig = encoding.NewOctetArray(dSig) + sig.EdDSASigR = encoding.NewOctetArray(ecSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1124,7 +1169,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.HMAC == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.SlhdsaSig == nil && sig.HMAC == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -1147,6 +1192,9 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = ed448.SignatureSize case ExperimentalPubKeyAlgoHMAC: sigLength = int(sig.HMAC.EncodedLength()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + sigLength = int(sig.EdDSASigR.EncodedLength()) + sigLength += int(sig.MldsaSig.EncodedLength()) default: panic("impossible") } @@ -1255,6 +1303,11 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { err = ed448.WriteSignature(w, sig.EdSig) case ExperimentalPubKeyAlgoHMAC: _, err = w.Write(sig.HMAC.EncodedBytes()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(sig.EdDSASigR.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.MldsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/read.go b/openpgp/read.go index 483857b08..10bc1899b 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -23,6 +23,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -118,7 +121,9 @@ 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, packet.ExperimentalPubKeyAlgoAEAD: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 37c2223f7..7bf3bc090 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "math/bits" "os" + "strconv" "strings" "testing" @@ -959,3 +960,91 @@ func TestReadV5Messages(t *testing.T) { t.Error("expected no signature error, got:", md.SignatureError) } } + +var pqcDraftVectors = map[string]struct { + armoredPrivateKey string + armoredPublicKey string + fingerprints []string + armoredMessages []string +}{ + // Update with fresh vectors + //"v6_Ed25519_ML-KEM-768+X25519": { + // v6Ed25519Mlkem768X25519PrivateTestVector, + // v6Ed25519Mlkem768X25519PublicTestVector, + // []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, + // []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + //}, +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + secretKey, err := ReadArmoredKeyRing(strings.NewReader(test.armoredPrivateKey)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != test.armoredPublicKey { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessages { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgReader, err := armor.Decode(strings.NewReader(armoredMessage)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) + } +} diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 77282c0ea..f8ef7fabc 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -473,3 +473,176 @@ jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD =8TxH -----END PGP PRIVATE KEY BLOCK----- ` + +// PQC keys and messages +const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +//const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +/d8lkIgM +-----END PGP PRIVATE KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +-----END PGP PUBLIC KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PrivateMessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPtBiEGJj40tpk451PcZ8qO43ZSeVE14OFuSIhxA8EdcwffQO1pvDRTpyIxERdP +Zf0JNCpG7uBqOXUty4vHAu/wCUmXFiutlBnRlG9O2jx2gaNp/HpAQeYmHwdDroFo +MGisG0RVOigKCVqjEgSCwmk0KLyGl6jFowNA9cMfi/pf6uU9PaweMGWmlgVyXDr0 +2qf/jsjEx87yeL3t6yi2YIFXCitLc+vaqWjd3/8qBOcoTf/TpPXMNPmzmffh8xZx +bU25jlzB25dHXRLmwnFUlz3PU7voCQNhBtJiMSXmCzbb26BWrB+YVNvxStokvDBG +pnP+lGcUIJUJpPgSoJeZLp5CWSl/UPTiuz6blsddWpfYm8wa/7V/EzmZNKkvDZt4 +7vdaXBaZDnPsMTE1Tn/FIc6/13CUe2rHDqcdLKIQ1bKRTpWH2BGqaX9a71XmxgR2 +kdTZ067m4xeRRGidL7/A5qklIEMumL+IyjC4zDvgtHBaGyCeDD12nK7paGhfuTxj +Qn4SQQvDvswUnUlmfPQbdMV1H02+lWHk7i4QpK2vrnKOd6O7pOnWFQSMGg/L4lCx +pfztFSf5bUrYSrf/VoQJdfqLwTZ0cw8uQC7eoEOn419DcKOQA1G/cKNY/lSeYZMD +IAAMZZ6iIzXcSvwd5NZkISVuZO1uh/9rhg4ZTOb+rcI6RYb5GHQbEvFAw1RUNk28 +4Vr1F2aYPuYw2rltNlE/D2jns6+9inJYnDmExbWX7hIItJVwwhGPqW0s0bbntFZD +zqlivMUoiCla49ZNQ6m7t5HwEv7IUZcNz5PvHvy5SPlFuzAJf82bKPYhAaCC1fE9 +IBQEVLG9Kw+duKgS2HtKndNd9sN3Edgf24JpM6OzhjIfuO8hUUUSl88mh3YlBKmp +xbBHd01s6rr2WK/L4KifiL+Bi99k0QJjVRx4mgv5uKv6sdFKmBkcSIr6olNG5GHR +hWCKuNvIg0zL9WSB8Qeav4s6sCn4gEWgyLXZ33tF39OwJFGZJtk+F01hNrISCylW +cQ39tM58hK2vuqAFjvvyHmjwrQDnGMfOh+86yMipIrWF7AfzB+BVdWOkBynRMgws +45Ne2D4XyD6z8rgKqrQEKWspHdeYOxhmtLZFpg5uO06I6T944whwXWYTeGjBPsi2 +YJuWlgH1nuZ+sw1FTE93XCfRHiLNQ6wBYCI9Usw9abAmW7Jhxd0/Kx72BbwLDmWm +vD1iXsgyCA1uyAfj89Xs5EIhPXFsxE6dfJ13dZGJVZl6mRJwjJgZStSEycvtsbtU +84tj9A+XpPfyCmk7wIte1d71vPE3s8Wx1WFYSiwPyVJS/AALSvPdEs4vhON7EQOa +xmhX1xITEesRXKhfKynhfMPpOUPgP1ctkpAbC8RGsRtEyhnALgHYqBYCULP+Pbmk +x34Z3pYlVXaWqiU0VJobuMwQJvnvax0ipFOPFYr6HBYvAuUlCdD17phL7ZFmLQjY +qstC0VS7E3mpvzbpo2uR1RDvWf6x6YFPAQoI9ltJ1S/lQdeLVh1+FOXuXh57qMcp +rD9h0SH7PihV9SRdvR2vvWyn7ygFNPajy/8PTH15eEv/5g6ZWxs5CKvpz0hTqf8C +0lQCCQIMslhjNg7KUOTtedOwUxvAoHK/lZf4fpMbG2GW7r6OHwShQ/zNruQmR8qV +qJsN7xv8+utysXtt6SUgMPnF3oUp9HzBnCwHb/m/di69xNsYQAE= +-----END PGP MESSAGE-----` diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index c52d408fe..84d328c22 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,6 +21,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" @@ -403,6 +405,21 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { case packet.ExperimentalPubKeyAlgoHMAC: hash := algorithm.HashById[hashToHashId(config.Hash())] return symmetric.HMACGenerateKey(config.Random(), hash) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -410,6 +427,7 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // newDecrypter generates an encryption/decryption key. func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { + pubKeyAlgo := config.PublicKeyAlgorithm() switch config.PublicKeyAlgorithm() { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() @@ -448,6 +466,26 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem_x25519 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index 15fc1b857..42df5da42 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -111,6 +111,7 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 + isPQ := false var maxTime time.Time var selectedSubkeySelfSig *packet.Signature for i, subkey := range e.Subkeys { @@ -118,10 +119,11 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) if err == nil && isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && checkKeyRequirements(subkey.PublicKey, config) == nil && - (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) { + (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix() || (!isPQ && subkey.IsPQ())) { candidateSubkey = i selectedSubkeySelfSig = subkeySelfSig maxTime = subkeySelfSig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -213,6 +215,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config } // Iterate the keys to find the newest, unexpired one. + isPQ := false candidateSubkey := -1 var maxTime time.Time var selectedSubkeySelfSig *packet.Signature @@ -223,10 +226,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config (flags&packet.KeyFlagSign == 0 || isValidSigningKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && checkKeyRequirements(subkey.PublicKey, config) == nil && (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkeySelfSig.CreationTime selectedSubkeySelfSig = subkeySelfSig + isPQ = subkey.IsPQ() } } diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index 0f0b946fa..fd4b986c0 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -26,6 +26,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -136,9 +139,9 @@ 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, packet.ExperimentalPubKeyAlgoAEAD: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 73d9be036..08607667b 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "math/bits" "os" + "strconv" "strings" "testing" @@ -29,6 +30,13 @@ func readerFromHex(s string) io.Reader { return bytes.NewBuffer(data) } +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + func TestReadKeyRing(t *testing.T) { kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) if err != nil { @@ -1062,9 +1070,91 @@ func TestReadMessageWithSignOnly(t *testing.T) { } } -func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { - _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) - if err != nil { - t.Error("could not read keyring", err) +var pqcDraftVectors = map[string]struct { + armoredPrivateKey string + armoredPublicKey string + fingerprints []string + armoredMessages []string +}{ + // TODO: Update with fresh test vectors + /* + "v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + },*/ +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + secretKey, err := ReadArmoredKeyRing(strings.NewReader(test.armoredPrivateKey)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != test.armoredPublicKey { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessages { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgReader, err := armor.Decode(strings.NewReader(armoredMessage)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) } } diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 050598aac..2e8a68680 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -806,3 +806,176 @@ jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD =8TxH -----END PGP PRIVATE KEY BLOCK----- ` + +// PQC keys and messages +const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +// const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +/d8lkIgM +-----END PGP PRIVATE KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +-----END PGP PUBLIC KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PrivateMessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPtBiEGJj40tpk451PcZ8qO43ZSeVE14OFuSIhxA8EdcwffQO1pvDRTpyIxERdP +Zf0JNCpG7uBqOXUty4vHAu/wCUmXFiutlBnRlG9O2jx2gaNp/HpAQeYmHwdDroFo +MGisG0RVOigKCVqjEgSCwmk0KLyGl6jFowNA9cMfi/pf6uU9PaweMGWmlgVyXDr0 +2qf/jsjEx87yeL3t6yi2YIFXCitLc+vaqWjd3/8qBOcoTf/TpPXMNPmzmffh8xZx +bU25jlzB25dHXRLmwnFUlz3PU7voCQNhBtJiMSXmCzbb26BWrB+YVNvxStokvDBG +pnP+lGcUIJUJpPgSoJeZLp5CWSl/UPTiuz6blsddWpfYm8wa/7V/EzmZNKkvDZt4 +7vdaXBaZDnPsMTE1Tn/FIc6/13CUe2rHDqcdLKIQ1bKRTpWH2BGqaX9a71XmxgR2 +kdTZ067m4xeRRGidL7/A5qklIEMumL+IyjC4zDvgtHBaGyCeDD12nK7paGhfuTxj +Qn4SQQvDvswUnUlmfPQbdMV1H02+lWHk7i4QpK2vrnKOd6O7pOnWFQSMGg/L4lCx +pfztFSf5bUrYSrf/VoQJdfqLwTZ0cw8uQC7eoEOn419DcKOQA1G/cKNY/lSeYZMD +IAAMZZ6iIzXcSvwd5NZkISVuZO1uh/9rhg4ZTOb+rcI6RYb5GHQbEvFAw1RUNk28 +4Vr1F2aYPuYw2rltNlE/D2jns6+9inJYnDmExbWX7hIItJVwwhGPqW0s0bbntFZD +zqlivMUoiCla49ZNQ6m7t5HwEv7IUZcNz5PvHvy5SPlFuzAJf82bKPYhAaCC1fE9 +IBQEVLG9Kw+duKgS2HtKndNd9sN3Edgf24JpM6OzhjIfuO8hUUUSl88mh3YlBKmp +xbBHd01s6rr2WK/L4KifiL+Bi99k0QJjVRx4mgv5uKv6sdFKmBkcSIr6olNG5GHR +hWCKuNvIg0zL9WSB8Qeav4s6sCn4gEWgyLXZ33tF39OwJFGZJtk+F01hNrISCylW +cQ39tM58hK2vuqAFjvvyHmjwrQDnGMfOh+86yMipIrWF7AfzB+BVdWOkBynRMgws +45Ne2D4XyD6z8rgKqrQEKWspHdeYOxhmtLZFpg5uO06I6T944whwXWYTeGjBPsi2 +YJuWlgH1nuZ+sw1FTE93XCfRHiLNQ6wBYCI9Usw9abAmW7Jhxd0/Kx72BbwLDmWm +vD1iXsgyCA1uyAfj89Xs5EIhPXFsxE6dfJ13dZGJVZl6mRJwjJgZStSEycvtsbtU +84tj9A+XpPfyCmk7wIte1d71vPE3s8Wx1WFYSiwPyVJS/AALSvPdEs4vhON7EQOa +xmhX1xITEesRXKhfKynhfMPpOUPgP1ctkpAbC8RGsRtEyhnALgHYqBYCULP+Pbmk +x34Z3pYlVXaWqiU0VJobuMwQJvnvax0ipFOPFYr6HBYvAuUlCdD17phL7ZFmLQjY +qstC0VS7E3mpvzbpo2uR1RDvWf6x6YFPAQoI9ltJ1S/lQdeLVh1+FOXuXh57qMcp +rD9h0SH7PihV9SRdvR2vvWyn7ygFNPajy/8PTH15eEv/5g6ZWxs5CKvpz0hTqf8C +0lQCCQIMslhjNg7KUOTtedOwUxvAoHK/lZf4fpMbG2GW7r6OHwShQ/zNruQmR8qV +qJsN7xv8+utysXtt6SUgMPnF3oUp9HzBnCwHb/m/di69xNsYQAE= +-----END PGP MESSAGE-----` diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go index 3e9fc1890..5e631fae1 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -208,3 +208,15 @@ func (s *Subkey) LatestValidBindingSignature(date time.Time, config *packet.Conf } return } + +// IsPQ returns true if the algorithm is Post-Quantum safe +func (s *Subkey) IsPQ() bool { + switch s.PublicKey.PubKeyAlgo { + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } + +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 398f63f47..249c423c9 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -610,6 +610,8 @@ func encrypt( // Override the time to select the encryption key with the provided one. timeForEncryptionKey = *params.EncryptionTime } + + allPQ := len(encryptKeys) > 0 for i, recipient := range append(to, toHidden...) { var ok bool encryptKeys[i], ok = recipient.EncryptionKey(timeForEncryptionKey, config) @@ -617,6 +619,10 @@ func encrypt( return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := recipient.PrimarySelfSignature(timeForEncryptionKey, config) if primarySelfSignature == nil { return nil, errors.StructuralError("entity without a self-signature") @@ -643,8 +649,12 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -1047,6 +1057,14 @@ func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { } } } + case packet.PubKeyAlgoMldsa65Ed25519: + return []uint8{ + hashToHashId(crypto.SHA3_256), + } + case packet.PubKeyAlgoMldsa87Ed448: + return []uint8{ + hashToHashId(crypto.SHA3_512), + } } return []uint8{ hashToHashId(crypto.SHA256), diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index 285508626..69609b3d4 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -435,33 +435,6 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { - keyRingHex string - isSigned bool - okV6 bool -}{ - { - testKeys1And2PrivateHex, - false, - true, - }, - { - testKeys1And2PrivateHex, - true, - true, - }, - { - dsaElGamalTestKeysHex, - false, - false, - }, - { - dsaElGamalTestKeysHex, - true, - false, - }, -} - func TestIntendedRecipientsEncryption(t *testing.T) { var config = &packet.Config{ V6Keys: true, @@ -675,129 +648,176 @@ func TestMultiSignEncryption(t *testing.T) { } } -func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) +var testEncryptionTests = map[string]struct { + keyRingHex string + isSigned bool + okV6 bool +}{ + "Simple": { + testKeys1And2PrivateHex, + false, + true, + }, + "Simple_signed": { + testKeys1And2PrivateHex, + true, + true, + }, + "DSA_ElGamal": { + dsaElGamalTestKeysHex, + false, + false, + }, + "DSA_ElGamal_signed": { + dsaElGamalTestKeysHex, + true, + false, + }, + // TODO: Add test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ +} - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) +func TestEncryption(t *testing.T) { + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - config := allowAllAlgorithmsConfig - config.DefaultCompressionAlgo = compAlgo - config.CompressionConfig = compConf - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed []*Entity + if test.isSigned { + signed = kring[:1] } - config.AEADConfig = &aeadConf - } - var signers []*Entity - if signed != nil { - signers = []*Entity{signed} - } - w, err := Encrypt(buf, kring[:1], nil, signers, nil /* no hints */, &config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } - - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = compAlgo + config.CompressionConfig = compConf + config.DefaultCipher = packet.CipherAES256 + + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], nil, signed, nil /* no hints */, &config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + return + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := signKey.PublicKey.KeyId - if len(md.SignatureCandidates) < 1 { - t.Error("no candidate signature found") + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) } - if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignatureCandidates[0].SignedByEntity == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") - encryptKey, _ := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("no candidate signature found") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%s: message signed by wrong key id, got: %v, want: %v", name, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%s: failed to find the signing Entity", name) + } + } - if test.isSigned { - if !md.IsVerified { - t.Errorf("not verified despite all data read") + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) } - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + + encryptKey, out := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + if !out { + t.Fatalf("#%s: No encryption key found", name) } - if md.Signature == nil { - t.Error("signature missing") + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - } + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) + } + + if test.isSigned { + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } diff --git a/openpgp/write.go b/openpgp/write.go index b0f6ef7b0..542a40f20 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -279,6 +279,11 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)") } + if signer != nil { + // Handle signature algorithms that force a specific hash. + hash = signer.PubKeyAlgo.HandleSpecificHash(hash) + } + var salt []byte if signer != nil { var opsVersion = 3 @@ -391,6 +396,7 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // AEAD is used only if config enables it and every key supports it aeadSupported := config.AEAD() != nil + allPQ := len(to) > 0 for i := range to { var ok bool encryptKeys[i], ok = to[i].EncryptionKey(config.Now()) @@ -398,6 +404,10 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := to[i].PrimarySelfSignature() if primarySelfSignature == nil { return nil, errors.InvalidArgumentError("entity without a self-signature") @@ -424,8 +434,12 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -560,11 +574,12 @@ func (s signatureWriter) Close() error { func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { sigLifetimeSecs := config.SigLifetime() + hash := signer.PubKeyAlgo.HandleSpecificHash(config.Hash()) return &packet.Signature{ Version: signer.Version, SigType: sigType, PubKeyAlgo: signer.PubKeyAlgo, - Hash: config.Hash(), + Hash: hash, CreationTime: config.Now(), IssuerKeyId: &signer.KeyId, IssuerFingerprint: signer.Fingerprint, diff --git a/openpgp/write_test.go b/openpgp/write_test.go index 315e73233..bcda90846 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -522,148 +522,162 @@ func TestSymmetricEncryptionSEIPDv2RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { +var testEncryptionTests = map[string]struct { keyRingHex string isSigned bool okV6 bool }{ - { + "Simple": { testKeys1And2PrivateHex, false, true, }, - { + "Simple_signed": { testKeys1And2PrivateHex, true, true, }, - { + "DSA_ElGamal": { dsaElGamalTestKeysHex, false, false, }, - { + "DSA_ElGamal_signed": { dsaElGamalTestKeysHex, true, false, }, + //TODO: Update test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ } func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) - - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - var signed *Entity - if test.isSigned { - signed = kring[0] - } - - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - var config = &packet.Config{ - DefaultCompressionAlgo: compAlgo, - CompressionConfig: compConf, - } - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed *Entity + if test.isSigned { + signed = kring[0] } - config.AEADConfig = &aeadConf - } - w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + var config = &packet.Config{ + DefaultCompressionAlgo: compAlgo, + CompressionConfig: compConf, + DefaultCipher: packet.CipherAES256, + } - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime) - expectedKeyId := signKey.PublicKey.KeyId - if md.SignedByKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId) + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignedBy == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - encryptKey, _ := kring[0].EncryptionKey(testTime) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime) + expectedKeyId := signKey.PublicKey.KeyId + if md.SignedByKeyId != expectedKeyId { + t.Errorf("Message signed by wrong key id, got: %v, want: %v", *md.SignedBy, expectedKeyId) + } + if md.SignedBy == nil { + t.Error("#Failed to find the signing Entity") + } + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } - if test.isSigned { - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + encryptKey, _ := kring[0].EncryptionKey(testTime) + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - if md.Signature == nil { - t.Error("signature missing") + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) } - } + + if test.isSigned { + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -784,7 +798,8 @@ ParsePackets: case *packet.EncryptedKey: // This packet contains the decryption key encrypted to a public key. switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue From 6bd2845e3d0cd66a951cc4a08735534d2da200c6 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 5 Nov 2024 17:33:41 +0100 Subject: [PATCH 05/14] Update to draft-ietf-openpgp-persistent-symmetric-keys-00 --- openpgp/internal/algorithm/aead.go | 5 ++ openpgp/internal/algorithm/cipher.go | 2 +- .../internal/encoding/short_byte_string.go | 50 --------------- .../encoding/short_byte_string_test.go | 61 ------------------- openpgp/key_generation.go | 5 +- openpgp/keys_test.go | 47 +++++++------- openpgp/packet/encrypted_key.go | 48 ++++++--------- openpgp/packet/encrypted_key_test.go | 19 ++---- openpgp/packet/packet.go | 4 +- openpgp/packet/private_key.go | 42 +------------ openpgp/packet/public_key.go | 37 ++++++----- openpgp/packet/signature.go | 9 +-- openpgp/packet/signature_test.go | 2 +- openpgp/read_write_test_data.go | 33 ++++++---- openpgp/symmetric/aead.go | 40 ++++++------ openpgp/symmetric/hmac.go | 31 +++------- openpgp/v2/key_generation.go | 3 +- openpgp/v2/keys_test.go | 49 ++++++++------- openpgp/v2/read_write_test_data.go | 33 ++++++---- 19 files changed, 186 insertions(+), 334 deletions(-) delete mode 100644 openpgp/internal/encoding/short_byte_string.go delete mode 100644 openpgp/internal/encoding/short_byte_string_test.go diff --git a/openpgp/internal/algorithm/aead.go b/openpgp/internal/algorithm/aead.go index d06706518..02d26a862 100644 --- a/openpgp/internal/algorithm/aead.go +++ b/openpgp/internal/algorithm/aead.go @@ -12,6 +12,11 @@ import ( // operation. type AEADMode uint8 +// Id returns the algorithm ID, as a byte, of mode. +func (mode AEADMode) Id() uint8 { + return uint8(mode) +} + // Supported modes of operation (see RFC4880bis [EAX] and RFC7253) const ( AEADModeEAX = AEADMode(1) diff --git a/openpgp/internal/algorithm/cipher.go b/openpgp/internal/algorithm/cipher.go index c76a75bcd..df3e5396c 100644 --- a/openpgp/internal/algorithm/cipher.go +++ b/openpgp/internal/algorithm/cipher.go @@ -46,7 +46,7 @@ var CipherById = map[uint8]Cipher{ type CipherFunction uint8 -// ID returns the algorithm Id, as a byte, of cipher. +// Id returns the algorithm ID, as a byte, of cipher. func (sk CipherFunction) Id() uint8 { return uint8(sk) } diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go deleted file mode 100644 index 0c3b91233..000000000 --- a/openpgp/internal/encoding/short_byte_string.go +++ /dev/null @@ -1,50 +0,0 @@ -package encoding - -import ( - "io" -) - -type ShortByteString struct { - length uint8 - data []byte -} - -func NewShortByteString(data []byte) *ShortByteString { - byteLength := uint8(len(data)) - - return &ShortByteString{byteLength, data} -} - -func (byteString *ShortByteString) Bytes() []byte { - return byteString.data -} - -func (byteString *ShortByteString) BitLength() uint16 { - return uint16(byteString.length) * 8 -} - -func (byteString *ShortByteString) EncodedBytes() []byte { - encodedLength := [1]byte{ - uint8(byteString.length), - } - return append(encodedLength[:], byteString.data...) -} - -func (byteString *ShortByteString) EncodedLength() uint16 { - return uint16(byteString.length) + 1 -} - -func (byteString *ShortByteString) ReadFrom(r io.Reader) (int64, error) { - var lengthBytes [1]byte - if n, err := io.ReadFull(r, lengthBytes[:]); err != nil { - return int64(n), err - } - - byteString.length = uint8(lengthBytes[0]) - - byteString.data = make([]byte, byteString.length) - if n, err := io.ReadFull(r, byteString.data); err != nil { - return int64(n + 1), err - } - return int64(byteString.length + 1), nil -} diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go deleted file mode 100644 index 37510a355..000000000 --- a/openpgp/internal/encoding/short_byte_string_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package encoding - -import ( - "bytes" - "testing" -) - -var octetStreamTests = []struct { - data []byte -}{ - { - data: []byte{0x0, 0x0, 0x0}, - }, - { - data: []byte{0x1, 0x2, 0x03}, - }, - { - data: make([]byte, 255), - }, -} - -func TestShortByteString(t *testing.T) { - for i, test := range octetStreamTests { - octetStream := NewShortByteString(test.data) - - if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { - t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) - } - - expectedBitLength := uint16(len(test.data)) * 8 - if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { - t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) - } - - expectedEncodedLength := uint16(len(test.data)) + 1 - if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { - t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) - } - - encodedBytes := octetStream.EncodedBytes() - if !bytes.Equal(encodedBytes[1:], test.data) { - t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) - } - - encodedLength := int(encodedBytes[0]) - if encodedLength != len(test.data) { - t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) - } - - newStream := new(ShortByteString) - newStream.ReadFrom(bytes.NewReader(encodedBytes)) - - if !checkEquality(newStream, octetStream) { - t.Errorf("#%d: bad parsing of encoded octet stream", i) - } - } -} - -func checkEquality(left *ShortByteString, right *ShortByteString) bool { - return (left.length == right.length) && (bytes.Equal(left.data, right.data)) -} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index df4fab454..3b15eeb2c 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -383,9 +383,10 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) - case packet.ExperimentalPubKeyAlgoAEAD: + case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey cipher := algorithm.CipherFunction(config.Cipher()) - return symmetric.AEADGenerateKey(config.Random(), cipher) + aead := algorithm.AEADMode(config.AEAD().Mode()) + return symmetric.AEADGenerateKey(config.Random(), cipher, aead) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 8bddbeb7a..f418b3044 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -1214,16 +1214,13 @@ func TestAddHMACSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { t.Error("parsed wrong cipher id") } - if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { - t.Error("parsed wrong binding hash") - } } func TestSerializeSymmetricSubkeyError(t *testing.T) { @@ -1235,13 +1232,13 @@ func TestSerializeSymmetricSubkeyError(t *testing.T) { buf := bytes.NewBuffer(nil) w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) - entity.PrimaryKey.PubKeyAlgo = 100 + entity.PrimaryKey.PubKeyAlgo = 128 err = entity.Serialize(w) if err == nil { t.Fatal(err) } - entity.PrimaryKey.PubKeyAlgo = 101 + entity.PrimaryKey.PubKeyAlgo = 129 err = entity.Serialize(w) if err == nil { t.Fatal(err) @@ -1292,15 +1289,15 @@ func TestAddAEADSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { t.Error("parsed wrong cipher id") } - if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { - t.Error("parsed wrong binding hash") + if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { + t.Error("parsed wrong aead mode") } } @@ -1344,11 +1341,11 @@ func TestNoSymmetricKeySerialized(t *testing.T) { t.Error("Private key was serialized with public") } - firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash - i = bytes.Index(w.Bytes(), firstBindingHash[:]) + firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed + i = bytes.Index(w.Bytes(), firstFpSeed[:]) - secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash - k = bytes.Index(w.Bytes(), secondBindingHash[:]) + secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed + k = bytes.Index(w.Bytes(), secondFpSeed[:]) if (i > 0) || (k > 0) { t.Errorf("Symmetric public key metadata exported %d %d", i, k) } @@ -2055,17 +2052,19 @@ mQ00BF00000BCAD0000000000000000000000000000000000000000000000000 func TestSymmetricKeys(t *testing.T) { data := `-----BEGIN PGP PRIVATE KEY BLOCK----- -xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ -n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef -D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO -8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF -ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT -X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 -OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC -ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR -TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB -TxGVotQ4A/0u0VbOMEUfnrI8Fms= -=RdCW +xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 +NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT +eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF +BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu +b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l +BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA +QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs +AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ +yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb +3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p +Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT +=ywfD -----END PGP PRIVATE KEY BLOCK----- ` keys, err := ReadArmoredKeyRing(strings.NewReader(data)) diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index bdc33812b..4ef72a02b 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -17,7 +17,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" @@ -36,15 +35,12 @@ type EncryptedKey struct { CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt - encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, and PQC keys + encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, AEAD and PQC keys encryptedMPI2 encoding.Field // Only valid in Elgamal, ECDH and PQC keys encryptedMPI3 encoding.Field // Only valid in PQC keys ephemeralPublicX25519 *x25519.PublicKey // used for x25519 ephemeralPublicX448 *x448.PublicKey // used for x448 encryptedSession []byte // used for x25519 and x448 - - nonce []byte - aeadMode algorithm.AEADMode } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -142,20 +138,11 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { return } case ExperimentalPubKeyAlgoAEAD: - var aeadMode [1]byte - if _, err = readFull(r, aeadMode[:]); err != nil { - return - } - e.aeadMode = algorithm.AEADMode(aeadMode[0]) - nonceLength := e.aeadMode.NonceLength() - e.nonce = make([]byte, nonceLength) - if _, err = readFull(r, e.nonce); err != nil { - return - } - e.encryptedMPI1 = new(encoding.ShortByteString) - if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { - return + ivAndCiphertext, err := io.ReadAll(r) + if err != nil { + return err } + e.encryptedMPI1 = encoding.NewOctetArray(ivAndCiphertext) case PubKeyAlgoMlkem768X25519: if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 32, 1088, e.Version == 6); err != nil { return err @@ -224,7 +211,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession) case ExperimentalPubKeyAlgoAEAD: priv := priv.PrivateKey.(*symmetric.AEADPrivateKey) - b, err = priv.Decrypt(e.nonce, e.encryptedMPI1.Bytes(), e.aeadMode) + b, err = priv.Decrypt(e.encryptedMPI1.Bytes(), priv.PublicKey.AEADMode) case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: ecE := e.encryptedMPI1.Bytes() kE := e.encryptedMPI2.Bytes() @@ -466,7 +453,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph case PubKeyAlgoX448: return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) case ExperimentalPubKeyAlgoAEAD: - return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) + return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock) case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return serializeEncryptedKeyMlkem(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*mlkem_ecdh.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly, ExperimentalPubKeyAlgoHMAC: @@ -610,20 +597,16 @@ func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub * return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) } -func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte, config *AEADConfig) error { - mode := algorithm.AEADMode(config.Mode()) - iv, ciphertextRaw, err := pub.Encrypt(rand, keyBlock, mode) +func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte) error { + mode := pub.AEADMode + iv, ciphertext, err := pub.Encrypt(rand, keyBlock, mode) if err != nil { return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) } - ciphertextShortByteString := encoding.NewShortByteString(ciphertextRaw) - - buffer := append([]byte{byte(mode)}, iv...) - buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) - packetLen := len(header) /* header length */ - packetLen += int(len(buffer)) + packetLen += int(len(iv)) + packetLen += int(len(ciphertext)) err = serializeHeader(w, packetTypeEncryptedKey, packetLen) if err != nil { @@ -635,7 +618,12 @@ func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub * return err } - _, err = w.Write(buffer) + _, err = w.Write(iv[:]) + if err != nil { + return err + } + + _, err = w.Write(ciphertext) return err } diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index 5ed0a8ed3..b52bb51fa 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -16,7 +16,6 @@ import ( "crypto" "crypto/rsa" - "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -341,11 +340,9 @@ func TestSerializingEncryptedKey(t *testing.T) { } func TestSymmetricallyEncryptedKey(t *testing.T) { - const encryptedKeyHex = "c14f03999bd17d726446da64018cb4d628ae753c646b81f87f21269cd733df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + const encryptedKeyHex = "c13d03999bd17d726446da80df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" - expectedNonce := []byte{0x8c, 0xb4, 0xd6, 0x28, 0xae, 0x75, 0x3c, 0x64, 0x6b, 0x81, 0xf8, 0x7f, 0x21, 0x26, 0x9c, 0xd7} - - expectedCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + expectedIvAndCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} p, err := Read(readerFromHex(encryptedKeyHex)) if err != nil { @@ -357,15 +354,7 @@ func TestSymmetricallyEncryptedKey(t *testing.T) { t.Fatalf("didn't parse and EncryptedKey, got %#v", p) } - if ek.aeadMode != algorithm.AEADModeEAX { - t.Errorf("Parsed wrong aead mode, got %d, expected: 1", ek.aeadMode) - } - - if !bytes.Equal(expectedNonce, ek.nonce) { - t.Errorf("Parsed wrong nonce, got %x, expected %x", ek.nonce, expectedNonce) - } - - if !bytes.Equal(expectedCiphertext, ek.encryptedMPI1.Bytes()) { - t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedCiphertext) + if !bytes.Equal(expectedIvAndCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedIvAndCiphertext) } } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index f65efb2e5..7dfdb2a3e 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -507,8 +507,8 @@ const ( PubKeyAlgoEd25519 PublicKeyAlgorithm = 27 PubKeyAlgoEd448 PublicKeyAlgorithm = 28 - ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 100 - ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 101 + ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 128 + ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 129 // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index aa0bfd61d..ba40066c7 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -543,19 +543,11 @@ func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error { } func serializeAEADPrivateKey(w io.Writer, priv *symmetric.AEADPrivateKey) (err error) { - _, err = w.Write(priv.HashSeed[:]) - if err != nil { - return - } _, err = w.Write(priv.Key) return } func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err error) { - _, err = w.Write(priv.HashSeed[:]) - if err != nil { - return - } _, err = w.Write(priv.Key) return err } @@ -1199,17 +1191,11 @@ func (pk *PrivateKey) parseAEADPrivateKey(data []byte) (err error) { aeadPriv := new(symmetric.AEADPrivateKey) aeadPriv.PublicKey = *pubKey - copy(aeadPriv.HashSeed[:], data[:32]) - priv := make([]byte, pubKey.Cipher.KeySize()) - copy(priv, data[32:]) + copy(priv, data[:]) aeadPriv.Key = priv aeadPriv.PublicKey.Key = aeadPriv.Key - if err = validateAEADParameters(aeadPriv); err != nil { - return - } - pk.PrivateKey = aeadPriv pk.PublicKey.PublicKey = &aeadPriv.PublicKey return @@ -1221,38 +1207,16 @@ func (pk *PrivateKey) parseHMACPrivateKey(data []byte) (err error) { hmacPriv := new(symmetric.HMACPrivateKey) hmacPriv.PublicKey = *pubKey - copy(hmacPriv.HashSeed[:], data[:32]) - priv := make([]byte, pubKey.Hash.Size()) - copy(priv, data[32:]) - hmacPriv.Key = data[32:] + copy(priv, data[:]) + hmacPriv.Key = priv[:] hmacPriv.PublicKey.Key = hmacPriv.Key - if err = validateHMACParameters(hmacPriv); err != nil { - return - } - pk.PrivateKey = hmacPriv pk.PublicKey.PublicKey = &hmacPriv.PublicKey return } -func validateAEADParameters(priv *symmetric.AEADPrivateKey) error { - return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) -} - -func validateHMACParameters(priv *symmetric.HMACPrivateKey) error { - return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) -} - -func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { - expectedBindingHash := symmetric.ComputeBindingHash(seed) - if !bytes.Equal(expectedBindingHash, bindingHash[:]) { - return errors.KeyInvalidError("symmetric: wrong binding hash") - } - return nil -} - // parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, seedLen int) (err error) { diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 823f82d07..231acd503 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -712,21 +712,22 @@ func (pk *PublicKey) parseEd448(r io.Reader) (err error) { } func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { - var cipher [1]byte - _, err = readFull(r, cipher[:]) + var algOctets [2]byte + _, err = readFull(r, algOctets[:]) if err != nil { return } - var bindingHash [32]byte - _, err = readFull(r, bindingHash[:]) + var fpSeed [32]byte + _, err = readFull(r, fpSeed[:]) if err != nil { return } symmetric := &symmetric.AEADPublicKey{ - Cipher: algorithm.CipherFunction(cipher[0]), - BindingHash: bindingHash, + Cipher: algorithm.CipherFunction(algOctets[0]), + AEADMode: algorithm.AEADMode(algOctets[1]), + FpSeed: fpSeed, } pk.PublicKey = symmetric @@ -739,7 +740,8 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { if err != nil { return } - bindingHash, err := readBindingHash(r) + var fpSeed [32]byte + _, err = readFull(r, fpSeed[:]) if err != nil { return } @@ -750,8 +752,8 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { } symmetric := &symmetric.HMACPublicKey{ - Hash: hmacHash, - BindingHash: bindingHash, + Hash: hmacHash, + FpSeed: fpSeed, } pk.PublicKey = symmetric @@ -884,9 +886,12 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length += ed25519.PublicKeySize case PubKeyAlgoEd448: length += ed448.PublicKeySize - case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: + case ExperimentalPubKeyAlgoAEAD: + length += 2 // Symmetric and AEAD algorithm octets + length += 32 // Fingerprint seed + case ExperimentalPubKeyAlgoHMAC: length += 1 // Hash octet - length += 32 // Binding hash + length += 32 // Fingerprint seed case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: length += uint32(pk.p.EncodedLength()) @@ -985,11 +990,11 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { return case ExperimentalPubKeyAlgoAEAD: symmKey := pk.PublicKey.(*symmetric.AEADPublicKey) - cipherOctet := [1]byte{symmKey.Cipher.Id()} - if _, err = w.Write(cipherOctet[:]); err != nil { + algOctets := [2]byte{symmKey.Cipher.Id(), symmKey.AEADMode.Id()} + if _, err = w.Write(algOctets[:]); err != nil { return } - _, err = w.Write(symmKey.BindingHash[:]) + _, err = w.Write(symmKey.FpSeed[:]) return case ExperimentalPubKeyAlgoHMAC: symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) @@ -997,7 +1002,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { if _, err = w.Write(hashOctet[:]); err != nil { return } - _, err = w.Write(symmKey.BindingHash[:]) + _, err = w.Write(symmKey.FpSeed[:]) return case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: @@ -1337,7 +1342,7 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { case PubKeyAlgoEd448: bitLength = ed448.PublicKeySize * 8 case ExperimentalPubKeyAlgoAEAD: - bitLength = 32 + bitLength = uint16(pk.PublicKey.(*symmetric.AEADPublicKey).Cipher.KeySize()) * 8 case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 034f39812..9742381e5 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -345,10 +345,11 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } case ExperimentalPubKeyAlgoHMAC: - sig.HMAC = new(encoding.ShortByteString) - if _, err = sig.HMAC.ReadFrom(r); err != nil { - return + hmac, err := io.ReadAll(r) + if err != nil { + return err } + sig.HMAC = encoding.NewOctetArray(hmac) case PubKeyAlgoMldsa65Ed25519: if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { return @@ -1033,7 +1034,7 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e case ExperimentalPubKeyAlgoHMAC: sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, nil) if err == nil { - sig.HMAC = encoding.NewShortByteString(sigdata) + sig.HMAC = encoding.NewOctetArray(sigdata) } case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: if sig.Version != 6 { diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index 19940387a..c2598280b 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -83,7 +83,7 @@ ltm2aQaG } func TestSymmetricSignatureRead(t *testing.T) { - const serializedPacket = "c272040165080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc400ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + const serializedPacket = "c271040181080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc0ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} packet, err := Read(readerFromHex(serializedPacket)) diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index f8ef7fabc..5c08f6bc1 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -459,18 +459,27 @@ byVJHvLO/XErtC+GNIJeMg== // A key that contains a persistent AEAD subkey const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv -/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k -zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ -AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA -/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX -mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy -RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 -3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW -CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l -PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv -jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD -=8TxH +xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM +34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z +zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ +kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB +AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK +HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg +dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG +6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS +ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU +AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F +Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR +HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j +dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck +iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf +PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA +AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L +7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe +AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj +089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI +=2WdX -----END PGP PRIVATE KEY BLOCK----- ` diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go index b9d389dc6..e13137e28 100644 --- a/openpgp/symmetric/aead.go +++ b/openpgp/symmetric/aead.go @@ -7,53 +7,55 @@ import ( type AEADPublicKey struct { Cipher algorithm.CipherFunction - BindingHash [32]byte + AEADMode algorithm.AEADMode + FpSeed [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for encryption. Thus, this `Key` field must never be exported + // publicly. Key []byte } type AEADPrivateKey struct { PublicKey AEADPublicKey - HashSeed [32]byte Key []byte } -func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (priv *AEADPrivateKey, err error) { priv, err = generatePrivatePartAEAD(rand, cipher) if err != nil { return } - priv.generatePublicPartAEAD(cipher) + priv.generatePublicPartAEAD(rand, cipher, aead) return } func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { priv = new(AEADPrivateKey) - var seed [32]byte - _, err = rand.Read(seed[:]) - if err != nil { - return - } - key := make([]byte, cipher.KeySize()) _, err = rand.Read(key) if err != nil { return } - - priv.HashSeed = seed priv.Key = key return } -func (priv *AEADPrivateKey) generatePublicPartAEAD(cipher algorithm.CipherFunction) (err error) { +func (priv *AEADPrivateKey) generatePublicPartAEAD(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (err error) { priv.PublicKey.Cipher = cipher + priv.PublicKey.AEADMode = aead - bindingHash := ComputeBindingHash(priv.HashSeed) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } priv.PublicKey.Key = make([]byte, len(priv.Key)) copy(priv.PublicKey.Key, priv.Key) - copy(priv.PublicKey.BindingHash[:], bindingHash) + copy(priv.PublicKey.FpSeed[:], seed[:]) return } @@ -66,10 +68,12 @@ func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AE return } -func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { - +func (priv *AEADPrivateKey) Decrypt(ivAndCiphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + nonceLength := mode.NonceLength() + iv := ivAndCiphertext[:nonceLength] + ciphertext := ivAndCiphertext[nonceLength:] block := priv.PublicKey.Cipher.New(priv.Key) aead := mode.New(block) - message, err = aead.Open(nil, nonce, ciphertext, nil) + message, err = aead.Open(nil, iv, ciphertext, nil) return } diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go index e9d61475c..50755f8ec 100644 --- a/openpgp/symmetric/hmac.go +++ b/openpgp/symmetric/hmac.go @@ -3,7 +3,6 @@ package symmetric import ( "crypto" "crypto/hmac" - "crypto/sha256" "io" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -11,8 +10,8 @@ import ( ) type HMACPublicKey struct { - Hash algorithm.Hash - BindingHash [32]byte + Hash algorithm.Hash + FpSeed [32]byte // While this is a "public" key, the symmetric key needs to be present here. // Symmetric cryptographic operations use the same key material for // signing and verifying, and go-crypto assumes that a public key type will @@ -23,7 +22,6 @@ type HMACPublicKey struct { type HMACPrivateKey struct { PublicKey HMACPublicKey - HashSeed [32]byte Key []byte } @@ -33,17 +31,12 @@ func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, return } - priv.generatePublicPartHMAC(hash) + priv.generatePublicPartHMAC(rand, hash) return } func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { priv = new(HMACPrivateKey) - var seed [32]byte - _, err = rand.Read(seed[:]) - if err != nil { - return - } key := make([]byte, hash.Size()) _, err = rand.Read(key) @@ -51,29 +44,25 @@ func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPri return } - priv.HashSeed = seed priv.Key = key return } -func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { +func (priv *HMACPrivateKey) generatePublicPartHMAC(rand io.Reader, hash algorithm.Hash) (err error) { priv.PublicKey.Hash = hash - bindingHash := ComputeBindingHash(priv.HashSeed) - copy(priv.PublicKey.BindingHash[:], bindingHash) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + copy(priv.PublicKey.FpSeed[:], seed[:]) priv.PublicKey.Key = make([]byte, len(priv.Key)) copy(priv.PublicKey.Key, priv.Key) return } -func ComputeBindingHash(seed [32]byte) []byte { - bindingHash := sha256.New() - bindingHash.Write(seed[:]) - - return bindingHash.Sum(nil) -} - func (priv *HMACPrivateKey) Public() crypto.PublicKey { return &priv.PublicKey } diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 84d328c22..4716d2b8d 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -465,7 +465,8 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x448.GenerateKey(config.Random()) case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey cipher := algorithm.CipherFunction(config.Cipher()) - return symmetric.AEADGenerateKey(config.Random(), cipher) + aead := algorithm.AEADMode(config.AEAD().Mode()) + return symmetric.AEADGenerateKey(config.Random(), cipher, aead) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index c9d277340..35c30f279 100644 --- a/openpgp/v2/keys_test.go +++ b/openpgp/v2/keys_test.go @@ -2068,16 +2068,13 @@ func TestAddHMACSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { t.Error("parsed wrong cipher id") } - if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { - t.Error("parsed wrong binding hash") - } } func TestSerializeSymmetricSubkeyError(t *testing.T) { @@ -2089,13 +2086,13 @@ func TestSerializeSymmetricSubkeyError(t *testing.T) { buf := bytes.NewBuffer(nil) w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) - entity.PrimaryKey.PubKeyAlgo = 100 + entity.PrimaryKey.PubKeyAlgo = 128 err = entity.Serialize(w) if err == nil { t.Fatal(err) } - entity.PrimaryKey.PubKeyAlgo = 101 + entity.PrimaryKey.PubKeyAlgo = 129 err = entity.Serialize(w) if err == nil { t.Fatal(err) @@ -2146,15 +2143,15 @@ func TestAddAEADSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { t.Error("parsed wrong cipher id") } - if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { - t.Error("parsed wrong binding hash") + if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { + t.Error("parsed wrong aead mode") } } @@ -2198,11 +2195,11 @@ func TestNoSymmetricKeySerialized(t *testing.T) { t.Error("Private key was serialized with public") } - firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash - i = bytes.Index(w.Bytes(), firstBindingHash[:]) + firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed + i = bytes.Index(w.Bytes(), firstFpSeed[:]) - secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash - k = bytes.Index(w.Bytes(), secondBindingHash[:]) + secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed + k = bytes.Index(w.Bytes(), secondFpSeed[:]) if (i > 0) || (k > 0) { t.Errorf("Symmetric public key metadata exported %d %d", i, k) } @@ -2211,18 +2208,20 @@ func TestNoSymmetricKeySerialized(t *testing.T) { func TestSymmetricKeys(t *testing.T) { data := `-----BEGIN PGP PRIVATE KEY BLOCK----- - -xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ -n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef -D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO -8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF -ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT -X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 -OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC -ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR -TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB -TxGVotQ4A/0u0VbOMEUfnrI8Fms= -=RdCW + +xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 +NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT +eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF +BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu +b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l +BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA +QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs +AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ +yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X +RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb +3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p +Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT +=ywfD -----END PGP PRIVATE KEY BLOCK----- ` keys, err := ReadArmoredKeyRing(strings.NewReader(data)) diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 2e8a68680..8a778f656 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -792,18 +792,27 @@ EBeLgD8oZHVsH3NLjPakPw== // A key that contains a persistent AEAD subkey const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv -/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k -zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ -AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA -/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX -mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy -RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 -3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW -CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l -PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv -jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD -=8TxH +xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM +34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z +zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ +kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB +AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK +HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg +dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG +6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS +ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU +AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F +Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR +HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j +dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck +iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf +PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA +AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L +7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe +AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj +089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI +=2WdX -----END PGP PRIVATE KEY BLOCK----- ` From 41edb67599667c8a736340d0f0debb6326912ffa Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 12 Jul 2022 16:40:23 +0200 Subject: [PATCH 06/14] Update GitHub workflow branches --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 39883d5fe..6874dcae8 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: From b989c8bdfe9a7d5870854fbf50cff2b9cc6d1f3a Mon Sep 17 00:00:00 2001 From: larabr Date: Tue, 12 Jul 2022 16:43:06 +0200 Subject: [PATCH 07/14] Add support for automatic forwarding (#54) --- openpgp/ecdh/ecdh.go | 105 +++- openpgp/ecdh/ecdh_test.go | 36 +- openpgp/errors/errors.go | 2 + openpgp/forwarding.go | 163 +++++ openpgp/forwarding_test.go | 224 +++++++ openpgp/internal/ecc/curve25519.go | 3 +- openpgp/internal/ecc/curve25519/curve25519.go | 122 ++++ .../ecc/curve25519/curve25519_test.go | 89 +++ openpgp/internal/ecc/curve25519/field/fe.go | 416 +++++++++++++ .../ecc/curve25519/field/fe_alias_test.go | 126 ++++ .../internal/ecc/curve25519/field/fe_amd64.go | 16 + .../internal/ecc/curve25519/field/fe_amd64.s | 379 ++++++++++++ .../ecc/curve25519/field/fe_amd64_noasm.go | 12 + .../internal/ecc/curve25519/field/fe_arm64.go | 16 + .../internal/ecc/curve25519/field/fe_arm64.s | 43 ++ .../ecc/curve25519/field/fe_arm64_noasm.go | 12 + .../ecc/curve25519/field/fe_bench_test.go | 36 ++ .../ecc/curve25519/field/fe_generic.go | 264 +++++++++ .../internal/ecc/curve25519/field/fe_test.go | 558 ++++++++++++++++++ openpgp/keys.go | 10 +- openpgp/packet/encrypted_key.go | 30 + openpgp/packet/forwarding.go | 36 ++ openpgp/packet/public_key.go | 53 +- openpgp/packet/signature.go | 13 +- openpgp/v2/forwarding.go | 159 +++++ openpgp/v2/forwarding_test.go | 253 ++++++++ openpgp/v2/keys.go | 15 +- 27 files changed, 3161 insertions(+), 30 deletions(-) create mode 100644 openpgp/forwarding.go create mode 100644 openpgp/forwarding_test.go create mode 100644 openpgp/internal/ecc/curve25519/curve25519.go create mode 100644 openpgp/internal/ecc/curve25519/curve25519_test.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_alias_test.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_amd64.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_amd64.s create mode 100644 openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_arm64.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_arm64.s create mode 100644 openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_bench_test.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_generic.go create mode 100644 openpgp/internal/ecc/curve25519/field/fe_test.go create mode 100644 openpgp/packet/forwarding.go create mode 100644 openpgp/v2/forwarding.go create mode 100644 openpgp/v2/forwarding_test.go 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 0eb3937b3..33f8d029a 100644 --- a/openpgp/errors/errors.go +++ b/openpgp/errors/errors.go @@ -73,6 +73,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/internal/ecc/curve25519.go b/openpgp/internal/ecc/curve25519.go index 888767c4e..a6721ff98 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 da3809df3..34bffb97e 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -381,7 +381,7 @@ func (el EntityList) KeysByIdUsage(id uint64, requiredUsage byte) (keys []Key) { func (el EntityList) DecryptionKeys() (keys []Key) { for _, e := range el { for _, subKey := range e.Subkeys { - if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { + if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications || subKey.Sig.FlagForward) { keys = append(keys, Key{e, subKey.PublicKey, subKey.PrivateKey, subKey.Sig, subKey.Revocations}) } } @@ -772,7 +772,7 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo // signatures from other entities. No private key material will be output. func (e *Entity) Serialize(w io.Writer) error { if e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || - e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { return errors.InvalidArgumentError("Can't serialize symmetric primary key") } err := e.PrimaryKey.Serialize(w) @@ -804,14 +804,16 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { - // The types of keys below are only useful as private keys. Thus, the // public key packets contain no meaningful information and do not need // to be serialized. + // Prevent public key export for forwarding keys, see forwarding section 4.1. if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || - subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + subkey.Sig.FlagForward { continue } + err = subkey.PublicKey.Serialize(w) if err != nil { return err diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 4ef72a02b..5cbc966d4 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -481,6 +481,36 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } +func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { + if e.Algo != PubKeyAlgoECDH { + return nil, errors.InvalidArgumentError("invalid PKESK") + } + + if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { + return nil, errors.InvalidArgumentError("invalid key id in PKESK") + } + + ephemeral := e.encryptedMPI1.Bytes() + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, instance.ProxyParameter) + if err != nil { + return nil, err + } + + wrappedKey := e.encryptedMPI2.Bytes() + copiedWrappedKey := make([]byte, len(wrappedKey)) + copy(copiedWrappedKey, wrappedKey) + + transformed = &EncryptedKey{ + Version: e.Version, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), + } + + return transformed, nil +} + func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) if err != nil { 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/public_key.go b/openpgp/packet/public_key.go index 231acd503..794cebec3 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,6 +5,7 @@ package packet import ( + "bytes" "crypto" "crypto/dsa" "crypto/rsa" @@ -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 { @@ -556,11 +577,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 { @@ -571,10 +594,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 } @@ -1190,6 +1226,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 9742381e5..fe36cc4f9 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -38,7 +38,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -134,8 +134,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. @@ -617,6 +618,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 } @@ -1421,6 +1425,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp 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/keys.go b/openpgp/v2/keys.go index 42df5da42..e2d3b4cc1 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -166,12 +166,12 @@ func (e *Entity) DecryptionKeys(id uint64, date time.Time, config *packet.Config for _, subkey := range e.Subkeys { subkeySelfSig, err := subkey.LatestValidBindingSignature(date, config) if err == nil && - (config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && + (config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && (id == 0 || subkey.PublicKey.KeyId == id) { keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, subkeySelfSig}) } } - if config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { + if config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) } return @@ -641,8 +641,11 @@ func (e *Entity) Serialize(w io.Writer) error { // The types of keys below are only useful as private keys. Thus, the // public key packets contain no meaningful information and do not need // to be serialized. + // Prevent public key export for forwarding keys, see forwarding section 4.1. + subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{}, nil) if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || - subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + (err == nil && subKeySelfSig.FlagForward) { continue } if err := subkey.Serialize(w, false); err != nil { @@ -803,3 +806,9 @@ func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgo signature.FlagsValid && (signature.FlagEncryptCommunications || signature.FlagEncryptStorage) } + +func isValidDecryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanEncrypt() && + signature.FlagsValid && + (signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage) +} From cee65ce1c9f8b265433105a7d4c33cdfa489e5d5 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 12 Nov 2024 18:26:56 +0100 Subject: [PATCH 08/14] Revert "[v2] Use AEAD if all public keys support it" This reverts commit 63e3da1cfd65188df420a89a2ce111cb619547e8. --- openpgp/v2/write.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 249c423c9..8509f6fe9 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -594,8 +594,8 @@ func encrypt( encryptKeys := make([]Key, len(to)+len(toHidden)) config := params.Config - // AEAD is used if every key supports it - aeadSupported := true + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil var intendedRecipients []*packet.Recipient // Intended Recipient Fingerprint subpacket SHOULD be used when creating a signed and encrypted message From b60d7cb764463b5a3d734b84d2cdf019e8ca416d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 14 Nov 2024 14:22:29 +0100 Subject: [PATCH 09/14] Revert "Update to draft-ietf-openpgp-persistent-symmetric-keys-00" This reverts commit 99debaab3e5c86dfe4a43b6d53575f4ddea5e5ad. --- openpgp/internal/algorithm/aead.go | 5 -- openpgp/internal/algorithm/cipher.go | 2 +- .../internal/encoding/short_byte_string.go | 50 +++++++++++++++ .../encoding/short_byte_string_test.go | 61 +++++++++++++++++++ openpgp/key_generation.go | 5 +- openpgp/keys_test.go | 47 +++++++------- openpgp/packet/encrypted_key.go | 48 +++++++++------ openpgp/packet/encrypted_key_test.go | 19 ++++-- openpgp/packet/packet.go | 4 +- openpgp/packet/private_key.go | 42 ++++++++++++- openpgp/packet/public_key.go | 37 +++++------ openpgp/packet/signature.go | 9 ++- openpgp/packet/signature_test.go | 2 +- openpgp/read_write_test_data.go | 33 ++++------ openpgp/symmetric/aead.go | 40 ++++++------ openpgp/symmetric/hmac.go | 31 +++++++--- openpgp/v2/key_generation.go | 3 +- openpgp/v2/keys_test.go | 49 +++++++-------- openpgp/v2/read_write_test_data.go | 33 ++++------ 19 files changed, 334 insertions(+), 186 deletions(-) create mode 100644 openpgp/internal/encoding/short_byte_string.go create mode 100644 openpgp/internal/encoding/short_byte_string_test.go diff --git a/openpgp/internal/algorithm/aead.go b/openpgp/internal/algorithm/aead.go index 02d26a862..d06706518 100644 --- a/openpgp/internal/algorithm/aead.go +++ b/openpgp/internal/algorithm/aead.go @@ -12,11 +12,6 @@ import ( // operation. type AEADMode uint8 -// Id returns the algorithm ID, as a byte, of mode. -func (mode AEADMode) Id() uint8 { - return uint8(mode) -} - // Supported modes of operation (see RFC4880bis [EAX] and RFC7253) const ( AEADModeEAX = AEADMode(1) diff --git a/openpgp/internal/algorithm/cipher.go b/openpgp/internal/algorithm/cipher.go index df3e5396c..c76a75bcd 100644 --- a/openpgp/internal/algorithm/cipher.go +++ b/openpgp/internal/algorithm/cipher.go @@ -46,7 +46,7 @@ var CipherById = map[uint8]Cipher{ type CipherFunction uint8 -// Id returns the algorithm ID, as a byte, of cipher. +// ID returns the algorithm Id, as a byte, of cipher. func (sk CipherFunction) Id() uint8 { return uint8(sk) } diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go new file mode 100644 index 000000000..0c3b91233 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string.go @@ -0,0 +1,50 @@ +package encoding + +import ( + "io" +) + +type ShortByteString struct { + length uint8 + data []byte +} + +func NewShortByteString(data []byte) *ShortByteString { + byteLength := uint8(len(data)) + + return &ShortByteString{byteLength, data} +} + +func (byteString *ShortByteString) Bytes() []byte { + return byteString.data +} + +func (byteString *ShortByteString) BitLength() uint16 { + return uint16(byteString.length) * 8 +} + +func (byteString *ShortByteString) EncodedBytes() []byte { + encodedLength := [1]byte{ + uint8(byteString.length), + } + return append(encodedLength[:], byteString.data...) +} + +func (byteString *ShortByteString) EncodedLength() uint16 { + return uint16(byteString.length) + 1 +} + +func (byteString *ShortByteString) ReadFrom(r io.Reader) (int64, error) { + var lengthBytes [1]byte + if n, err := io.ReadFull(r, lengthBytes[:]); err != nil { + return int64(n), err + } + + byteString.length = uint8(lengthBytes[0]) + + byteString.data = make([]byte, byteString.length) + if n, err := io.ReadFull(r, byteString.data); err != nil { + return int64(n + 1), err + } + return int64(byteString.length + 1), nil +} diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go new file mode 100644 index 000000000..37510a355 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var octetStreamTests = []struct { + data []byte +}{ + { + data: []byte{0x0, 0x0, 0x0}, + }, + { + data: []byte{0x1, 0x2, 0x03}, + }, + { + data: make([]byte, 255), + }, +} + +func TestShortByteString(t *testing.T) { + for i, test := range octetStreamTests { + octetStream := NewShortByteString(test.data) + + if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { + t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) + } + + expectedBitLength := uint16(len(test.data)) * 8 + if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { + t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) + } + + expectedEncodedLength := uint16(len(test.data)) + 1 + if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { + t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) + } + + encodedBytes := octetStream.EncodedBytes() + if !bytes.Equal(encodedBytes[1:], test.data) { + t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) + } + + encodedLength := int(encodedBytes[0]) + if encodedLength != len(test.data) { + t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) + } + + newStream := new(ShortByteString) + newStream.ReadFrom(bytes.NewReader(encodedBytes)) + + if !checkEquality(newStream, octetStream) { + t.Errorf("#%d: bad parsing of encoded octet stream", i) + } + } +} + +func checkEquality(left *ShortByteString, right *ShortByteString) bool { + return (left.length == right.length) && (bytes.Equal(left.data, right.data)) +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 3b15eeb2c..df4fab454 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -383,10 +383,9 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) - case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + case packet.ExperimentalPubKeyAlgoAEAD: cipher := algorithm.CipherFunction(config.Cipher()) - aead := algorithm.AEADMode(config.AEAD().Mode()) - return symmetric.AEADGenerateKey(config.Random(), cipher, aead) + return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index f418b3044..8bddbeb7a 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -1214,13 +1214,16 @@ func TestAddHMACSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { t.Error("parsed wrong cipher id") } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } } func TestSerializeSymmetricSubkeyError(t *testing.T) { @@ -1232,13 +1235,13 @@ func TestSerializeSymmetricSubkeyError(t *testing.T) { buf := bytes.NewBuffer(nil) w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) - entity.PrimaryKey.PubKeyAlgo = 128 + entity.PrimaryKey.PubKeyAlgo = 100 err = entity.Serialize(w) if err == nil { t.Fatal(err) } - entity.PrimaryKey.PubKeyAlgo = 129 + entity.PrimaryKey.PubKeyAlgo = 101 err = entity.Serialize(w) if err == nil { t.Fatal(err) @@ -1289,15 +1292,15 @@ func TestAddAEADSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { t.Error("parsed wrong cipher id") } - if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { - t.Error("parsed wrong aead mode") + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") } } @@ -1341,11 +1344,11 @@ func TestNoSymmetricKeySerialized(t *testing.T) { t.Error("Private key was serialized with public") } - firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed - i = bytes.Index(w.Bytes(), firstFpSeed[:]) + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) - secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed - k = bytes.Index(w.Bytes(), secondFpSeed[:]) + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) if (i > 0) || (k > 0) { t.Errorf("Symmetric public key metadata exported %d %d", i, k) } @@ -2052,19 +2055,17 @@ mQ00BF00000BCAD0000000000000000000000000000000000000000000000000 func TestSymmetricKeys(t *testing.T) { data := `-----BEGIN PGP PRIVATE KEY BLOCK----- -xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 -NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT -eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF -BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu -b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l -BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA -QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs -AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ -yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X -RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb -3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p -Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT -=ywfD +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW -----END PGP PRIVATE KEY BLOCK----- ` keys, err := ReadArmoredKeyRing(strings.NewReader(data)) diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 5cbc966d4..59f1c847f 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -17,6 +17,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" @@ -35,12 +36,15 @@ type EncryptedKey struct { CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt - encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, AEAD and PQC keys + encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, and PQC keys encryptedMPI2 encoding.Field // Only valid in Elgamal, ECDH and PQC keys encryptedMPI3 encoding.Field // Only valid in PQC keys ephemeralPublicX25519 *x25519.PublicKey // used for x25519 ephemeralPublicX448 *x448.PublicKey // used for x448 encryptedSession []byte // used for x25519 and x448 + + nonce []byte + aeadMode algorithm.AEADMode } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -138,11 +142,20 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { return } case ExperimentalPubKeyAlgoAEAD: - ivAndCiphertext, err := io.ReadAll(r) - if err != nil { - return err + var aeadMode [1]byte + if _, err = readFull(r, aeadMode[:]); err != nil { + return + } + e.aeadMode = algorithm.AEADMode(aeadMode[0]) + nonceLength := e.aeadMode.NonceLength() + e.nonce = make([]byte, nonceLength) + if _, err = readFull(r, e.nonce); err != nil { + return + } + e.encryptedMPI1 = new(encoding.ShortByteString) + if _, err = e.encryptedMPI1.ReadFrom(r); err != nil { + return } - e.encryptedMPI1 = encoding.NewOctetArray(ivAndCiphertext) case PubKeyAlgoMlkem768X25519: if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 32, 1088, e.Version == 6); err != nil { return err @@ -211,7 +224,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { b, err = x448.Decrypt(priv.PrivateKey.(*x448.PrivateKey), e.ephemeralPublicX448, e.encryptedSession) case ExperimentalPubKeyAlgoAEAD: priv := priv.PrivateKey.(*symmetric.AEADPrivateKey) - b, err = priv.Decrypt(e.encryptedMPI1.Bytes(), priv.PublicKey.AEADMode) + b, err = priv.Decrypt(e.nonce, e.encryptedMPI1.Bytes(), e.aeadMode) case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: ecE := e.encryptedMPI1.Bytes() kE := e.encryptedMPI2.Bytes() @@ -453,7 +466,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph case PubKeyAlgoX448: return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) case ExperimentalPubKeyAlgoAEAD: - return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock) + return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return serializeEncryptedKeyMlkem(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*mlkem_ecdh.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly, ExperimentalPubKeyAlgoHMAC: @@ -627,16 +640,20 @@ func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub * return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) } -func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte) error { - mode := pub.AEADMode - iv, ciphertext, err := pub.Encrypt(rand, keyBlock, mode) +func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte, config *AEADConfig) error { + mode := algorithm.AEADMode(config.Mode()) + iv, ciphertextRaw, err := pub.Encrypt(rand, keyBlock, mode) if err != nil { return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) } + ciphertextShortByteString := encoding.NewShortByteString(ciphertextRaw) + + buffer := append([]byte{byte(mode)}, iv...) + buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) + packetLen := len(header) /* header length */ - packetLen += int(len(iv)) - packetLen += int(len(ciphertext)) + packetLen += int(len(buffer)) err = serializeHeader(w, packetTypeEncryptedKey, packetLen) if err != nil { @@ -648,12 +665,7 @@ func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub * return err } - _, err = w.Write(iv[:]) - if err != nil { - return err - } - - _, err = w.Write(ciphertext) + _, err = w.Write(buffer) return err } diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index b52bb51fa..5ed0a8ed3 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -16,6 +16,7 @@ import ( "crypto" "crypto/rsa" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -340,9 +341,11 @@ func TestSerializingEncryptedKey(t *testing.T) { } func TestSymmetricallyEncryptedKey(t *testing.T) { - const encryptedKeyHex = "c13d03999bd17d726446da80df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + const encryptedKeyHex = "c14f03999bd17d726446da64018cb4d628ae753c646b81f87f21269cd733df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" - expectedIvAndCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + expectedNonce := []byte{0x8c, 0xb4, 0xd6, 0x28, 0xae, 0x75, 0x3c, 0x64, 0x6b, 0x81, 0xf8, 0x7f, 0x21, 0x26, 0x9c, 0xd7} + + expectedCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} p, err := Read(readerFromHex(encryptedKeyHex)) if err != nil { @@ -354,7 +357,15 @@ func TestSymmetricallyEncryptedKey(t *testing.T) { t.Fatalf("didn't parse and EncryptedKey, got %#v", p) } - if !bytes.Equal(expectedIvAndCiphertext, ek.encryptedMPI1.Bytes()) { - t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedIvAndCiphertext) + if ek.aeadMode != algorithm.AEADModeEAX { + t.Errorf("Parsed wrong aead mode, got %d, expected: 1", ek.aeadMode) + } + + if !bytes.Equal(expectedNonce, ek.nonce) { + t.Errorf("Parsed wrong nonce, got %x, expected %x", ek.nonce, expectedNonce) + } + + if !bytes.Equal(expectedCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedCiphertext) } } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 7dfdb2a3e..f65efb2e5 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -507,8 +507,8 @@ const ( PubKeyAlgoEd25519 PublicKeyAlgorithm = 27 PubKeyAlgoEd448 PublicKeyAlgorithm = 28 - ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 128 - ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 129 + ExperimentalPubKeyAlgoAEAD PublicKeyAlgorithm = 100 + ExperimentalPubKeyAlgoHMAC PublicKeyAlgorithm = 101 // Deprecated in RFC 4880, Section 13.5. Use key flags instead. PubKeyAlgoRSAEncryptOnly PublicKeyAlgorithm = 2 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index ba40066c7..aa0bfd61d 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -543,11 +543,19 @@ func serializeEd448PrivateKey(w io.Writer, priv *ed448.PrivateKey) error { } func serializeAEADPrivateKey(w io.Writer, priv *symmetric.AEADPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } _, err = w.Write(priv.Key) return } func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err error) { + _, err = w.Write(priv.HashSeed[:]) + if err != nil { + return + } _, err = w.Write(priv.Key) return err } @@ -1191,11 +1199,17 @@ func (pk *PrivateKey) parseAEADPrivateKey(data []byte) (err error) { aeadPriv := new(symmetric.AEADPrivateKey) aeadPriv.PublicKey = *pubKey + copy(aeadPriv.HashSeed[:], data[:32]) + priv := make([]byte, pubKey.Cipher.KeySize()) - copy(priv, data[:]) + copy(priv, data[32:]) aeadPriv.Key = priv aeadPriv.PublicKey.Key = aeadPriv.Key + if err = validateAEADParameters(aeadPriv); err != nil { + return + } + pk.PrivateKey = aeadPriv pk.PublicKey.PublicKey = &aeadPriv.PublicKey return @@ -1207,16 +1221,38 @@ func (pk *PrivateKey) parseHMACPrivateKey(data []byte) (err error) { hmacPriv := new(symmetric.HMACPrivateKey) hmacPriv.PublicKey = *pubKey + copy(hmacPriv.HashSeed[:], data[:32]) + priv := make([]byte, pubKey.Hash.Size()) - copy(priv, data[:]) - hmacPriv.Key = priv[:] + copy(priv, data[32:]) + hmacPriv.Key = data[32:] hmacPriv.PublicKey.Key = hmacPriv.Key + if err = validateHMACParameters(hmacPriv); err != nil { + return + } + pk.PrivateKey = hmacPriv pk.PublicKey.PublicKey = &hmacPriv.PublicKey return } +func validateAEADParameters(priv *symmetric.AEADPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateHMACParameters(priv *symmetric.HMACPrivateKey) error { + return validateCommonSymmetric(priv.HashSeed, priv.PublicKey.BindingHash) +} + +func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { + expectedBindingHash := symmetric.ComputeBindingHash(seed) + if !bytes.Equal(expectedBindingHash, bindingHash[:]) { + return errors.KeyInvalidError("symmetric: wrong binding hash") + } + return nil +} + // parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, seedLen int) (err error) { diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 794cebec3..b96469f07 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -748,22 +748,21 @@ func (pk *PublicKey) parseEd448(r io.Reader) (err error) { } func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { - var algOctets [2]byte - _, err = readFull(r, algOctets[:]) + var cipher [1]byte + _, err = readFull(r, cipher[:]) if err != nil { return } - var fpSeed [32]byte - _, err = readFull(r, fpSeed[:]) + var bindingHash [32]byte + _, err = readFull(r, bindingHash[:]) if err != nil { return } symmetric := &symmetric.AEADPublicKey{ - Cipher: algorithm.CipherFunction(algOctets[0]), - AEADMode: algorithm.AEADMode(algOctets[1]), - FpSeed: fpSeed, + Cipher: algorithm.CipherFunction(cipher[0]), + BindingHash: bindingHash, } pk.PublicKey = symmetric @@ -776,8 +775,7 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { if err != nil { return } - var fpSeed [32]byte - _, err = readFull(r, fpSeed[:]) + bindingHash, err := readBindingHash(r) if err != nil { return } @@ -788,8 +786,8 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { } symmetric := &symmetric.HMACPublicKey{ - Hash: hmacHash, - FpSeed: fpSeed, + Hash: hmacHash, + BindingHash: bindingHash, } pk.PublicKey = symmetric @@ -922,12 +920,9 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length += ed25519.PublicKeySize case PubKeyAlgoEd448: length += ed448.PublicKeySize - case ExperimentalPubKeyAlgoAEAD: - length += 2 // Symmetric and AEAD algorithm octets - length += 32 // Fingerprint seed - case ExperimentalPubKeyAlgoHMAC: + case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: length += 1 // Hash octet - length += 32 // Fingerprint seed + length += 32 // Binding hash case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: length += uint32(pk.p.EncodedLength()) @@ -1026,11 +1021,11 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { return case ExperimentalPubKeyAlgoAEAD: symmKey := pk.PublicKey.(*symmetric.AEADPublicKey) - algOctets := [2]byte{symmKey.Cipher.Id(), symmKey.AEADMode.Id()} - if _, err = w.Write(algOctets[:]); err != nil { + cipherOctet := [1]byte{symmKey.Cipher.Id()} + if _, err = w.Write(cipherOctet[:]); err != nil { return } - _, err = w.Write(symmKey.FpSeed[:]) + _, err = w.Write(symmKey.BindingHash[:]) return case ExperimentalPubKeyAlgoHMAC: symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) @@ -1038,7 +1033,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { if _, err = w.Write(hashOctet[:]); err != nil { return } - _, err = w.Write(symmKey.FpSeed[:]) + _, err = w.Write(symmKey.BindingHash[:]) return case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: @@ -1385,7 +1380,7 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { case PubKeyAlgoEd448: bitLength = ed448.PublicKeySize * 8 case ExperimentalPubKeyAlgoAEAD: - bitLength = uint16(pk.PublicKey.(*symmetric.AEADPublicKey).Cipher.KeySize()) * 8 + bitLength = 32 case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index fe36cc4f9..6c1ae22dc 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -346,11 +346,10 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } case ExperimentalPubKeyAlgoHMAC: - hmac, err := io.ReadAll(r) - if err != nil { - return err + sig.HMAC = new(encoding.ShortByteString) + if _, err = sig.HMAC.ReadFrom(r); err != nil { + return } - sig.HMAC = encoding.NewOctetArray(hmac) case PubKeyAlgoMldsa65Ed25519: if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { return @@ -1038,7 +1037,7 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e case ExperimentalPubKeyAlgoHMAC: sigdata, err := priv.PrivateKey.(crypto.Signer).Sign(config.Random(), digest, nil) if err == nil { - sig.HMAC = encoding.NewOctetArray(sigdata) + sig.HMAC = encoding.NewShortByteString(sigdata) } case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: if sig.Version != 6 { diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index c2598280b..19940387a 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -83,7 +83,7 @@ ltm2aQaG } func TestSymmetricSignatureRead(t *testing.T) { - const serializedPacket = "c271040181080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc0ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + const serializedPacket = "c272040165080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc400ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} packet, err := Read(readerFromHex(serializedPacket)) diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 5c08f6bc1..f8ef7fabc 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -459,27 +459,18 @@ byVJHvLO/XErtC+GNIJeMg== // A key that contains a persistent AEAD subkey const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM -34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z -zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ -kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v -cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB -AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK -HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg -dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG -6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS -ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU -AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F -Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR -HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j -dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck -iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf -PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA -AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L -7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe -AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj -089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI -=2WdX +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH -----END PGP PRIVATE KEY BLOCK----- ` diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go index e13137e28..b9d389dc6 100644 --- a/openpgp/symmetric/aead.go +++ b/openpgp/symmetric/aead.go @@ -7,55 +7,53 @@ import ( type AEADPublicKey struct { Cipher algorithm.CipherFunction - AEADMode algorithm.AEADMode - FpSeed [32]byte - // While this is a "public" key, the symmetric key needs to be present here. - // Symmetric cryptographic operations use the same key material for - // signing and verifying, and go-crypto assumes that a public key type will - // be used for encryption. Thus, this `Key` field must never be exported - // publicly. + BindingHash [32]byte Key []byte } type AEADPrivateKey struct { PublicKey AEADPublicKey + HashSeed [32]byte Key []byte } -func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (priv *AEADPrivateKey, err error) { +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { priv, err = generatePrivatePartAEAD(rand, cipher) if err != nil { return } - priv.generatePublicPartAEAD(rand, cipher, aead) + priv.generatePublicPartAEAD(cipher) return } func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { priv = new(AEADPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + key := make([]byte, cipher.KeySize()) _, err = rand.Read(key) if err != nil { return } + + priv.HashSeed = seed priv.Key = key return } -func (priv *AEADPrivateKey) generatePublicPartAEAD(rand io.Reader, cipher algorithm.CipherFunction, aead algorithm.AEADMode) (err error) { +func (priv *AEADPrivateKey) generatePublicPartAEAD(cipher algorithm.CipherFunction) (err error) { priv.PublicKey.Cipher = cipher - priv.PublicKey.AEADMode = aead - var seed [32]byte - _, err = rand.Read(seed[:]) - if err != nil { - return - } + bindingHash := ComputeBindingHash(priv.HashSeed) priv.PublicKey.Key = make([]byte, len(priv.Key)) copy(priv.PublicKey.Key, priv.Key) - copy(priv.PublicKey.FpSeed[:], seed[:]) + copy(priv.PublicKey.BindingHash[:], bindingHash) return } @@ -68,12 +66,10 @@ func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AE return } -func (priv *AEADPrivateKey) Decrypt(ivAndCiphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { - nonceLength := mode.NonceLength() - iv := ivAndCiphertext[:nonceLength] - ciphertext := ivAndCiphertext[nonceLength:] +func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + block := priv.PublicKey.Cipher.New(priv.Key) aead := mode.New(block) - message, err = aead.Open(nil, iv, ciphertext, nil) + message, err = aead.Open(nil, nonce, ciphertext, nil) return } diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go index 50755f8ec..e9d61475c 100644 --- a/openpgp/symmetric/hmac.go +++ b/openpgp/symmetric/hmac.go @@ -3,6 +3,7 @@ package symmetric import ( "crypto" "crypto/hmac" + "crypto/sha256" "io" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -10,8 +11,8 @@ import ( ) type HMACPublicKey struct { - Hash algorithm.Hash - FpSeed [32]byte + Hash algorithm.Hash + BindingHash [32]byte // While this is a "public" key, the symmetric key needs to be present here. // Symmetric cryptographic operations use the same key material for // signing and verifying, and go-crypto assumes that a public key type will @@ -22,6 +23,7 @@ type HMACPublicKey struct { type HMACPrivateKey struct { PublicKey HMACPublicKey + HashSeed [32]byte Key []byte } @@ -31,12 +33,17 @@ func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, return } - priv.generatePublicPartHMAC(rand, hash) + priv.generatePublicPartHMAC(hash) return } func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { priv = new(HMACPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } key := make([]byte, hash.Size()) _, err = rand.Read(key) @@ -44,25 +51,29 @@ func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPri return } + priv.HashSeed = seed priv.Key = key return } -func (priv *HMACPrivateKey) generatePublicPartHMAC(rand io.Reader, hash algorithm.Hash) (err error) { +func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { priv.PublicKey.Hash = hash - var seed [32]byte - _, err = rand.Read(seed[:]) - if err != nil { - return - } - copy(priv.PublicKey.FpSeed[:], seed[:]) + bindingHash := ComputeBindingHash(priv.HashSeed) + copy(priv.PublicKey.BindingHash[:], bindingHash) priv.PublicKey.Key = make([]byte, len(priv.Key)) copy(priv.PublicKey.Key, priv.Key) return } +func ComputeBindingHash(seed [32]byte) []byte { + bindingHash := sha256.New() + bindingHash.Write(seed[:]) + + return bindingHash.Sum(nil) +} + func (priv *HMACPrivateKey) Public() crypto.PublicKey { return &priv.PublicKey } diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 4716d2b8d..84d328c22 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -465,8 +465,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x448.GenerateKey(config.Random()) case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey cipher := algorithm.CipherFunction(config.Cipher()) - aead := algorithm.AEADMode(config.AEAD().Mode()) - return symmetric.AEADGenerateKey(config.Random(), cipher, aead) + return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index 35c30f279..c9d277340 100644 --- a/openpgp/v2/keys_test.go +++ b/openpgp/v2/keys_test.go @@ -2068,13 +2068,16 @@ func TestAddHMACSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { t.Error("parsed wrong cipher id") } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } } func TestSerializeSymmetricSubkeyError(t *testing.T) { @@ -2086,13 +2089,13 @@ func TestSerializeSymmetricSubkeyError(t *testing.T) { buf := bytes.NewBuffer(nil) w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) - entity.PrimaryKey.PubKeyAlgo = 128 + entity.PrimaryKey.PubKeyAlgo = 100 err = entity.Serialize(w) if err == nil { t.Fatal(err) } - entity.PrimaryKey.PubKeyAlgo = 129 + entity.PrimaryKey.PubKeyAlgo = 101 err = entity.Serialize(w) if err == nil { t.Fatal(err) @@ -2143,15 +2146,15 @@ func TestAddAEADSubkey(t *testing.T) { t.Error("generated Public and Private Key differ") } - if !bytes.Equal(parsedPublicKey.FpSeed[:], generatedPublicKey.FpSeed[:]) { + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { t.Error("parsed wrong hash seed") } if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { t.Error("parsed wrong cipher id") } - if parsedPrivateKey.PublicKey.AEADMode.Id() != generatedPrivateKey.PublicKey.AEADMode.Id() { - t.Error("parsed wrong aead mode") + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") } } @@ -2195,11 +2198,11 @@ func TestNoSymmetricKeySerialized(t *testing.T) { t.Error("Private key was serialized with public") } - firstFpSeed := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).FpSeed - i = bytes.Index(w.Bytes(), firstFpSeed[:]) + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) - secondFpSeed := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).FpSeed - k = bytes.Index(w.Bytes(), secondFpSeed[:]) + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) if (i > 0) || (k > 0) { t.Errorf("Symmetric public key metadata exported %d %d", i, k) } @@ -2208,20 +2211,18 @@ func TestNoSymmetricKeySerialized(t *testing.T) { func TestSymmetricKeys(t *testing.T) { data := `-----BEGIN PGP PRIVATE KEY BLOCK----- - -xUoEZyoQrIEImuGs5gaOTekO00WQx6MDnyBPvxmpMiOgeVse7+aqarsAc8F5 -NFm3pVkFDZxX0MqRCPqCwsa/BXJGlrEdMAwSNckOV80xUGVyc2lzdGVudCBT -eW1tZXRyaWMgS2V5IDxwZXJzaXN0ZW50QGV4YW1wbGUub3JnPsKvBBOBCgCF -BYJnKhCsAwsJBwmQDqlD7wlMH9dFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu -b3BlbnBncGpzLm9yZ4pMjYSZvCHJsWo5/hQJ3qfDMVMnetCsdS4ZSR6oeO7l -BRUKCAwOBBYAAgECGQECmwMCHgEWIQSbMhUPoVGIuE9u9GAOqUPvCUwf1wAA -QXxcTdhWEMhv+uYj8lUjGbDiqMHc7oGQSattlK89H9KT18dLBGcqEKyACQPs -AUFGawprheOyMQEYmVQUCoTdw4SVAxPk3Wkdbd7YtQATgtwB+JTCDy4de8F+ -yKpsXCJEFrVCsVnFyyY3gH5Wgw5PwpoEGIEKAHAFgmcqEKwJkA6pQ+8JTB/X -RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdwNnP67WFb -3vwFQkTQHsuFKLqvtvpQdnDs9RmvPxLZUwKbDBYhBJsyFQ+hUYi4T270YA6p -Q+8JTB/XAAC0o7OPSjaqMfpfYDUewr7Ehi5kFRCDBwbxLWFryAiICULT -=ywfD + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW -----END PGP PRIVATE KEY BLOCK----- ` keys, err := ReadArmoredKeyRing(strings.NewReader(data)) diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 8a778f656..2e8a68680 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -792,27 +792,18 @@ EBeLgD8oZHVsH3NLjPakPw== // A key that contains a persistent AEAD subkey const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -xVgEZypNfBYJKwYBBAHaRw8BAQdAag5k2wQ5kNPa/BAhUuAucrG8o9p71riM -34x8NwQ9G1wAAP0cmDSK7NLI2LzyIQtLpAANHoAyLxkObT2N6SK9gTt6NQ4z -zRd0ZXN0IDx0ZXN0QGV4YW1wbGUub3JnPsLAEwQTFgoAhQWCZypNfAMLCQcJ -kH3vtREeAXvNRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v -cmdIXNnr8sRWIc56Ttw5TvcBQ4kBZDf7DwQPQQRchEoCwQUVCggMDgQWAAIB -AhkBApsDAh4BFiEELiytxINFTJSqscZgfe+1ER4Be80AAA8kAQCURpNRDBuK -HMHUUhyfs4ba3KXWZ8tu5Doqx8HXCHuovQEAj8pO//gt8PZlt6P0tVqZItsg -dkjH67KM5PdtlvSMrgfHXQRnKk18EgorBgEEAZdVAQUBAQdAVUVOljcQeIuG -6S2DyrqbO73UtqOK4kOXt5c238AOygwDAQgHAAD/VUjA1uCSGVb4tlz4h0PS -ewITrKGqO87MCd3ZUyM8VyAQ9cK+BBgWCgBwBYJnKk18CZB977URHgF7zUUU -AAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jn+hW1SjRxZh+F -Kpe+KXLtk9QJp/2ly/EbTv43hLi+/FsCmwwWIQQuLK3Eg0VMlKqxxmB977UR -HgF7zQAA33IA/RcTNF+3EBI273gWHy/tsSLJ1r05hJ7/DEN+KvIe7bNvAP4j -dGqPDRabcstbF+MmunFJoDSiuikYN1rdskDZ52+rAMdLBGcqTaiACQP6GAck -iE9MdrWMpykKn4MNfe5+3HQ+PvkLKSxhRwNZGwDHOv2+yJJNTcbgeC7Z/POf -PyOum0vrd35zd5LteFyRXhJlwr4EGBYKAHAFgmcqTagJkH3vtREeAXvNRRQA -AAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmdaLY3r2qR/IS3L -7Wa0Vewc1s90cf0OUpy3AVGPOKKGYQKbDBYhBC4srcSDRUyUqrHGYH3vtREe -AXvNAAAgcgD+IwOjsj+BB+qlIL/XEaccgIhT27NDKnBWtOGmyDZufwIA/idj -089k5VoCQMVWHQVDk8oumkxweFLNjkev5LeEm7QI -=2WdX +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH -----END PGP PRIVATE KEY BLOCK----- ` From ba255f07c286e41a4736bdd1ab1e48a809119f56 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 14 Nov 2024 09:33:04 +0100 Subject: [PATCH 10/14] feat: ML-KEM change key combiner to 2a mailinglist Implements the key combiner 2a from the OpenPGP mailinglist: https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ --- internal/kmac/kmac.go | 147 ------------------------------- internal/kmac/kmac_test.go | 142 ----------------------------- openpgp/mlkem_ecdh/mlkem_ecdh.go | 42 +++------ 3 files changed, 13 insertions(+), 318 deletions(-) delete mode 100644 internal/kmac/kmac.go delete mode 100644 internal/kmac/kmac_test.go diff --git a/internal/kmac/kmac.go b/internal/kmac/kmac.go deleted file mode 100644 index 695ff5e42..000000000 --- a/internal/kmac/kmac.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 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 kmac provides function for creating KMAC instances. -// KMAC is a Message Authentication Code that based on SHA-3 and -// specified in NIST Special Publication 800-185, "SHA-3 Derived Functions: -// cSHAKE, KMAC, TupleHash and ParallelHash" [1] -// -// [1] https://doi.org/10.6028/NIST.SP.800-185 -package kmac - -import ( - "encoding/binary" - "fmt" - "hash" - - "golang.org/x/crypto/sha3" -) - -const ( - // According to [1]: - // "When used as a MAC, applications of this Recommendation shall - // not select an output length L that is less than 32 bits, and - // shall only select an output length less than 64 bits after a - // careful risk analysis is performed." - // 64 bits was selected for safety. - kmacMinimumTagSize = 8 - rate128 = 168 - rate256 = 136 -) - -// KMAC specific context -type kmac struct { - sha3.ShakeHash // cSHAKE context and Read/Write operations - tagSize int // tag size - // initBlock is the KMAC specific initialization set of bytes. It is initialized - // by newKMAC function and stores the key, encoded by the method specified in 3.3 of [1]. - // It is stored here in order for Reset() to be able to put context into - // initial state. - initBlock []byte - rate int -} - -// NewKMAC128 returns a new KMAC hash providing 128 bits of security using -// the given key, which must have 16 bytes or more, generating the given tagSize -// bytes output and using the given customizationString. -// Note that unlike other hash implementations in the standard library, -// the returned Hash does not implement encoding.BinaryMarshaler -// or encoding.BinaryUnmarshaler. -func NewKMAC128(key []byte, tagSize int, customizationString []byte) (h hash.Hash, err error) { - c := sha3.NewCShake128([]byte("KMAC"), customizationString) - h = newKMAC(key, tagSize, c, rate128) - if len(key) < 16 { - return h, fmt.Errorf("kmac: key is too short with %d bytes: should be at least %d", len(key), 16) - } - return h, nil -} - -// NewKMAC256 returns a new KMAC hash providing 256 bits of security using -// the given key, which must have 32 bytes or more, generating the given tagSize -// bytes output and using the given customizationString. -// Note that unlike other hash implementations in the standard library, -// the returned Hash does not implement encoding.BinaryMarshaler -// or encoding.BinaryUnmarshaler. -func NewKMAC256(key []byte, tagSize int, customizationString []byte) (h hash.Hash, err error) { - c := sha3.NewCShake256([]byte("KMAC"), customizationString) - h = newKMAC(key, tagSize, c, rate256) - if len(key) < 32 { - return h, fmt.Errorf("kmac: key is too short with %d bytes: should be at least %d", len(key), 32) - } - return h, nil -} - -func newKMAC(key []byte, tagSize int, c sha3.ShakeHash, rate int) hash.Hash { - if tagSize < kmacMinimumTagSize { - panic("tagSize is too small") - } - k := &kmac{ShakeHash: c, tagSize: tagSize, rate: rate} - // leftEncode returns max 9 bytes - k.initBlock = make([]byte, 0, 9+len(key)) - k.initBlock = append(k.initBlock, leftEncode(uint64(len(key)*8))...) - k.initBlock = append(k.initBlock, key...) - k.Write(bytepad(k.initBlock, k.BlockSize())) - return k -} - -// Reset resets the hash to initial state. -func (k *kmac) Reset() { - k.ShakeHash.Reset() - k.Write(bytepad(k.initBlock, k.BlockSize())) -} - -// BlockSize returns the hash block size. -func (k *kmac) BlockSize() int { - return k.rate -} - -// Size returns the tag size. -func (k *kmac) Size() int { - return k.tagSize -} - -// Sum appends the current KMAC to b and returns the resulting slice. -// It does not change the underlying hash state. -func (k *kmac) Sum(b []byte) []byte { - dup := k.ShakeHash.Clone() - dup.Write(rightEncode(uint64(k.tagSize * 8))) - hash := make([]byte, k.tagSize) - dup.Read(hash) - return append(b, hash...) -} - -func bytepad(input []byte, w int) []byte { - // leftEncode always returns max 9 bytes - buf := make([]byte, 0, 9+len(input)+w) - buf = append(buf, leftEncode(uint64(w))...) - buf = append(buf, input...) - padlen := w - (len(buf) % w) - return append(buf, make([]byte, padlen)...) -} - -func leftEncode(value uint64) []byte { - var b [9]byte - binary.BigEndian.PutUint64(b[1:], value) - // Trim all but last leading zero bytes - i := byte(1) - for i < 8 && b[i] == 0 { - i++ - } - // Prepend number of encoded bytes - b[i-1] = 9 - i - return b[i-1:] -} - -func rightEncode(value uint64) []byte { - var b [9]byte - binary.BigEndian.PutUint64(b[:8], value) - // Trim all but last leading zero bytes - i := byte(0) - for i < 7 && b[i] == 0 { - i++ - } - // Append number of encoded bytes - b[8] = 8 - i - return b[i:] -} diff --git a/internal/kmac/kmac_test.go b/internal/kmac/kmac_test.go deleted file mode 100644 index 07bff8c1c..000000000 --- a/internal/kmac/kmac_test.go +++ /dev/null @@ -1,142 +0,0 @@ -/// Copyright 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 kmac_test implements a vector-based test suite for the cSHAKE KMAC implementation -package kmac_test - -import ( - "bytes" - "encoding/hex" - "fmt" - "hash" - "testing" - - "github.com/ProtonMail/go-crypto/internal/kmac" -) - -// Test vectors from -// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/cSHAKE_samples.pdf -var kmacTests = []struct { - security int - key, data, customization, tag string -}{ - { - 128, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "00010203", - "", - "E5780B0D3EA6F7D3A429C5706AA43A00FADBD7D49628839E3187243F456EE14E", - }, - { - 128, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "00010203", - "My Tagged Application", - "3B1FBA963CD8B0B59E8C1A6D71888B7143651AF8BA0A7070C0979E2811324AA5", - }, - { - 128, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", - "My Tagged Application", - "1F5B4E6CCA02209E0DCB5CA635B89A15E271ECC760071DFD805FAA38F9729230", - }, - { - 256, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "00010203", - "My Tagged Application", - "20C570C31346F703C9AC36C61C03CB64C3970D0CFC787E9B79599D273A68D2F7F69D4CC3DE9D104A351689F27CF6F5951F0103F33F4F24871024D9C27773A8DD", - }, - { - 256, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", - "", - "75358CF39E41494E949707927CEE0AF20A3FF553904C86B08F21CC414BCFD691589D27CF5E15369CBBFF8B9A4C2EB17800855D0235FF635DA82533EC6B759B69", - }, - { - 256, - "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F", - "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7", - "My Tagged Application", - "B58618F71F92E1D56C1B8C55DDD7CD188B97B4CA4D99831EB2699A837DA2E4D970FBACFDE50033AEA585F1A2708510C32D07880801BD182898FE476876FC8965", - }, -} - -func TestKMAC(t *testing.T) { - for i, test := range kmacTests { - key, err := hex.DecodeString(test.key) - if err != nil { - t.Errorf("error decoding KAT: %s", err) - } - tag, err := hex.DecodeString(test.tag) - if err != nil { - t.Errorf("error decoding KAT: %s", err) - } - var mac hash.Hash - if test.security == 128 { - mac, err = kmac.NewKMAC128(key, len(tag), []byte(test.customization)) - } else { - mac, err = kmac.NewKMAC256(key, len(tag), []byte(test.customization)) - } - if err != nil { - t.Fatal(err) - } - data, err := hex.DecodeString(test.data) - if err != nil { - t.Errorf("error decoding KAT: %s", err) - } - mac.Write(data) - computedTag := mac.Sum(nil) - if !bytes.Equal(tag, computedTag) { - t.Errorf("#%d: got %x, want %x", i, tag, computedTag) - } - if mac.Size() != len(tag) { - t.Errorf("#%d: Size() = %x, want %x", i, mac.Size(), len(tag)) - } - // Test if it works after Reset. - mac.Reset() - mac.Write(data) - computedTag = mac.Sum(nil) - if !bytes.Equal(tag, computedTag) { - t.Errorf("#%d: got %x, want %x", i, tag, computedTag) - } - // Test if Sum does not change state. - if len(data) > 1 { - mac.Reset() - mac.Write(data[0:1]) - mac.Sum(nil) - mac.Write(data[1:]) - computedTag = mac.Sum(nil) - if !bytes.Equal(tag, computedTag) { - t.Errorf("#%d: got %x, want %x", i, tag, computedTag) - } - } - } -} -func ExampleNewKMAC256() { - key := []byte("this is a secret key; you should generate a strong random key that's at least 32 bytes long") - tag := make([]byte, 16) - msg := []byte("The quick brown fox jumps over the lazy dog") - // Example 1: Simple KMAC - k, err := kmac.NewKMAC256(key, len(tag), []byte("Partition1")) - if err != nil { - panic(err) - } - k.Write(msg) - k.Sum(tag[:0]) - fmt.Println(hex.EncodeToString(tag)) - // Example 2: Different customization string produces different digest - k, err = kmac.NewKMAC256(key, 16, []byte("Partition2")) - if err != nil { - panic(err) - } - k.Write(msg) - k.Sum(tag[:0]) - fmt.Println(hex.EncodeToString(tag)) - // Output: - //3814d78758add078334b8ab9e5c4f942 - //3762371e99e1e01ab17742b95c0360da -} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index 686a25528..a8fafb655 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -7,7 +7,6 @@ import ( "fmt" "io" - "github.com/ProtonMail/go-crypto/internal/kmac" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "golang.org/x/crypto/sha3" @@ -19,7 +18,6 @@ import ( const ( maxSessionKeyLength = 64 - domainSeparator = "OpenPGPCompositeKDFv1" MlKemSeedLen = 64 ) @@ -140,8 +138,8 @@ func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg return keywrap.Unwrap(kek, ciphertext) } -// buildKey implements the composite KDF as specified in -// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-combiner +// buildKey implements the composite KDF 2a from +// https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { h := sha3.New256() @@ -160,35 +158,21 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK // mlkemEphemeral - the ML-KEM ciphertext encoded as an octet string // mlkemPublicKey - The ML-KEM public key of the recipient as an octet string // algId - the OpenPGP algorithm ID of the public-key encryption algorithm - // domainSeparator – the UTF-8 encoding of the string "OpenPGPCompositeKDFv1" // eccKeyShare - the ECDH key share encoded as an octet string // eccEphemeral - the ECDH ciphertext encoded as an octet string // eccPublicKey - The ECDH public key of the recipient as an octet string - // KEK = KMAC256( - // mlkemKeyShare || eccKeyShare, - // mlkemEphemeral || eccEphemeral || mlkemPublicKey || ecdhPublicKey || algId, - // 256 (32 bytes), - // domainSeparator - // ) - - kMacKeyBuffer := make([]byte, len(mlkemKeyShare)+len(eccKeyShare)) - copy(kMacKeyBuffer[:len(mlkemKeyShare)], mlkemKeyShare) - copy(kMacKeyBuffer[len(mlkemKeyShare):], eccKeyShare) - - k, err := kmac.NewKMAC256(kMacKeyBuffer, 32, []byte(domainSeparator)) - if err != nil { - return nil, err - } - - // kmac hash never returns an error - _, _ = k.Write(mlkemEphemeral) - _, _ = k.Write(eccEphemeral) - _, _ = k.Write(serializedMlkemPublicKey) - _, _ = k.Write(eccPublicKey) - _, _ = k.Write([]byte{pub.AlgId}) - - return k.Sum(nil), nil + // 2a. SHA3-256(mlkemKeyShare || eccKeyShare || eccEphemeral || eccPublicKey || Domain) + // where Domain is "Domain" for LAMPS, and "mlkemEphemeral || mlkemPublicKey || algId" for OpenPGP + h.Reset() + _, _ = h.Write(mlkemKeyShare) + _, _ = h.Write(eccKeyShare) + _, _ = h.Write(eccEphemeral) + _, _ = h.Write(eccPublicKey) + _, _ = h.Write(mlkemEphemeral) + _, _ = h.Write(serializedMlkemPublicKey) + _, _ = h.Write([]byte{pub.AlgId}) + return h.Sum(nil), nil } // Validate checks that the public key corresponds to the private key From 59cb1c871cde80e27176f2377738667baca67357 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 29 Nov 2024 10:23:26 +0100 Subject: [PATCH 11/14] feat: ML-KEM change key combiner to latest draft See: https://github.com/openpgp-pqc/draft-openpgp-pqc/pull/161 --- openpgp/mlkem_ecdh/mlkem_ecdh.go | 21 +- openpgp/read_test.go | 15 +- openpgp/read_write_test_data.go | 835 +++++++++++++++++++++++++------ 3 files changed, 696 insertions(+), 175 deletions(-) diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index a8fafb655..ea07c79c3 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -19,6 +19,7 @@ import ( const ( maxSessionKeyLength = 64 MlKemSeedLen = 64 + kdfContext = "OpenPGPCompositeKDFv1" ) type PublicKey struct { @@ -138,16 +139,11 @@ func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg return keywrap.Unwrap(kek, ciphertext) } -// buildKey implements the composite KDF 2a from -// https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ +// buildKey implements the composite KDF from +// https://github.com/openpgp-pqc/draft-openpgp-pqc/pull/161 func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { - h := sha3.New256() - - // SHA3 never returns error - _, _ = h.Write(eccSecretPoint) - _, _ = h.Write(eccEphemeral) - _, _ = h.Write(eccPublicKey) - eccKeyShare := h.Sum(nil) + /// Set the output `ecdhKeyShare` to `eccSecretPoint` + eccKeyShare := eccSecretPoint serializedMlkemPublicKey, err := mlkemPublicKey.MarshalBinary() if err != nil { @@ -162,9 +158,9 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK // eccEphemeral - the ECDH ciphertext encoded as an octet string // eccPublicKey - The ECDH public key of the recipient as an octet string - // 2a. SHA3-256(mlkemKeyShare || eccKeyShare || eccEphemeral || eccPublicKey || Domain) - // where Domain is "Domain" for LAMPS, and "mlkemEphemeral || mlkemPublicKey || algId" for OpenPGP - h.Reset() + // SHA3-256(mlkemKeyShare || eccKeyShare || eccEphemeral || eccPublicKey || + // mlkemEphemeral || mlkemPublicKey || algId || "OpenPGPCompositeKDFv1") + h := sha3.New256() _, _ = h.Write(mlkemKeyShare) _, _ = h.Write(eccKeyShare) _, _ = h.Write(eccEphemeral) @@ -172,6 +168,7 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK _, _ = h.Write(mlkemEphemeral) _, _ = h.Write(serializedMlkemPublicKey) _, _ = h.Write([]byte{pub.AlgId}) + _, _ = h.Write([]byte(kdfContext)) return h.Sum(nil), nil } diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 7bf3bc090..74040b544 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -967,13 +967,12 @@ var pqcDraftVectors = map[string]struct { fingerprints []string armoredMessages []string }{ - // Update with fresh vectors - //"v6_Ed25519_ML-KEM-768+X25519": { - // v6Ed25519Mlkem768X25519PrivateTestVector, - // v6Ed25519Mlkem768X25519PublicTestVector, - // []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, - // []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, - //}, + "v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"bf262b24177002ac8ae5dc6da47c056d22ab9906d47d07952b75c358021901ca", "48b94bce2f9771788f5feb74122d599989c400cc0f49108bc98e0ea7945e4838"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + }, } func TestPqcDraftVectors(t *testing.T) { @@ -1040,7 +1039,7 @@ func TestPqcDraftVectors(t *testing.T) { return } - if string(contents) != "Testing\n" { + if string(contents) != "Testing\r\n" { t.Fatalf("Decrypted message is wrong: %s", contents) } }) diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index f8ef7fabc..5e2ae370b 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -482,167 +482,692 @@ const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986 // PQC draft test vectors const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- -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 -/d8lkIgM +xcdLBlHQxoBrAAAHwGy44RkYq173huDlFbFTF0FbPOsQdZRherJYVBKlJKH8+IJe +miR3kgJaw42LGYryBJuCAavtf1M+Nyh3IR06sEJ226n+LLb72/uGCrlqRlUQkSbz +/EHjiOogerpgiuz6D8gHDqdNlwvtuO0Cw+CbCFAsDbSOp8zqwd3Qq1poCSLueLc1 +DIJe8TkZrmqbLA1LMyQAvT7ALQjGAP33wzKB1fGDv8napjWSiCgT03EdvRa39pEZ +I7Inu5y0RkYdsTKt03HkcApB3XM8n4xolj3NYrUc7f1Mqqlz+5d5ydcFRmnos+Cm +fDzj6cdaseT9IV8BfHjVNMXpozpxkr8Dwaz7a2SrSnJIp9N2k3nlCVMAGYj4qyWO +dmRjevSvJbPccX7ar4V7g+a/wr7Ec+beB+J3ya87SXI1PYj7wc6wIPCZRqxk+X9Z +JLESYmoOV3eQQwdOgjiWp5xWAn7LJ+MBC6cFcQRMfFPDuZlbwv2gQ6vX2HTpHqaO +oE86wKXYfpHqgTukExxFdx7A6JlAcxYi57g0xfCllxDD9wXDirsBMX4eGK/d+/1c +E9Cf7yTJQlQfL+Dtpsqr3qfrGHvxiwxVmzsp7yM9J75ZZoQpNC22jUgOG4U9r5N5 +Op30/340M1ysMKzVgduz+tE3ewU8Mm8lNe/TU5R2ZN5TDV5cT1O70MLzjPhECfRU +pKCyhnBVidNLZ7GGOmYQyj2zXcmQ0cM1By/ohWz2fnOxH702Y+QdRv0r52hlvCY9 +RoNnlax5tEN7zE8cVgLVfVN0UXU+klVSlHfLPMy4gZK2/s7LylVcabRhTE9MG3hU +fbtpA0KPQU4v8BartJNOwGAXx1qpCxppDZHWVnp8xO5cVhLENe0CoUn8S9wO2M6B +ggbn+35F7pMONASGH3uELt4XzibzNGFJCmUUPOOHfFPwrqtfjvYkPySLTTp21j47 +2ab+FfVBFBkK1FOlnz3ufA2U/0+LvRAIfdJIfRzES+Gx8g5u2eh7VO8pwmw2zexW +3Cp/4LNisLyg/vv+UgMdWfh66A5r6ssXN9MT8oytWcMcsng0FlmCVKehbea/8ziO +rVJ9RdGaiAv2EF1mm+5ZDi9asOemwdzX39277NfFYXcBpigqFEqMVbhoUGQu635Y +eZbNBuTSlq7s7hVgGzMszeImuv0cVFLkk5z/JvYkbO/cuGFcN9xYhHXj1R/6DNqf +JyRAKhBheMkxGNVSV+7Ot5V1mRMGpt3UmnkowooTGHux69oYFRx8gBvMNs/+Kbub +n+dgPA2fbwCR8kWc6jvewU4g0fjNiSZgFuM4EYMMAeEAZTFNw4Cm8TWZ6Wmo1Hax +aJVvnJWRGXF4TP4I6lX3am7dMiofK5Og4BxymDabzJKo8uiOYXkyUUTfWfzDGdpZ +bF+hW4qcvE650KP+Bf4rOOSfOJ6SFt2y0srSJpR+H3EfLk76WpDqiy1DlBpV8E74 +sEJK7CcwzsaSyxpUzk4N+KJAa7ahkL1/Rw9ZFf0PEkquMIOoK0MXlQ24jlTDCAD5 +7ynzhLqWfxhj6KNRbS2M8wVrf7WBjgjHlRIaWZFbpC6nUP0wqNXIG7ZlMfRQQjHH +jTlPFFe8kcq9Xcw3oKZmln4yHhstR960DFYGELvTR+9ozFiYoxgAwdJC7748EtvD +ASXWZqhQ1g3lXEDHbJ8PAdU3GxeYSeUiC80Hro/0uf20tLGU3kdyfbmMjBrnDzWF +bOwcm/76V15YIgFiOENXWivesqNY4eZ4MSB7ZcJm3wdvJ2ZsUzcsVmj0x7bxgld5 +hsl4uEJxKgVwUf0Hg8AJQJ/p0PjFiwWF9wttOZynrSfamWur/kmmhKPS0NqPFEXE +I70eMuPVCGI8oLvfK93RqZqGUsaURVrsDNH2fvx5sh1/iaXtl3fTcZyrcLYUck83 +4sY/KUi+/NHfZpj82EhVjOvQ/2f0tqADLeYXy1bFXE/T/6ooN/eEV8yKSwL43mjB +q9iaURTOFOqftc7Bv+IzOxCDjpYIoQKmHa8VeDSEMURBfn22amDA900dgwbEMpaP ++L/SrxkcIzYie//XYePcyhGjAuIQine/+vAE9fBZ4zS0tnoAsktlD7S0EPNKrlfi +H7kHNh9ueRwzOxUHomMzRzTet8zY3ZxXXYAqD9W6LiUbnK+4a7LtsF+xRSL9XsBN +YevQFSZ2i6d8Xoqx6hRB4GVGCopQSAixwnSqoYw1x9tL+u/rYS1hNeEFN06MBvry +CfKvy4ZeGXI3Pd0xyyqAcFyF7biYxWPoNkm8mQEvuKdAFvSXRQd9lrkcH+QEkiML +eJUw2jPVxG39r8wJ1yrHzPsizQWUb+s99OO4heuhw7PTeIIT7cg217iAJL+GrkCb +oEn6BdFr96D8kn4SHGbyW51ZvTmwIAfpOqp3Lt4ph4//N3HJ1yhVmF4rtgjGSoJ9 +nJIngjMb20dRis55Pe1n7w5CXM+VTo+YUTKokiBLNtIlNRHtWdvE6C29se2Zs0Tt +hNn4QzQNXWh4k3o1zD51V8FLUz6hROGelleVf8MPcKtYPIJn1JDRnhgfT3bNnbh0 +Dp8AKmeymVAKVPTU8mQDHmVQy4V6DGQ6Enn7sBXzKobOEGpI3+JFYnd3Deu2dT/2 +Fh0ildKLKNiZW21YjRMGBJcf8mySmg2XBoWX0hQAIwMP3lbrMuvxBRAiK1fjPROx +VX/XVz27xQu8fJ8SjgdWUkF0/U0ltlE3OgHnVrHyEeuLg4pU7h8JJK/G3m2kOMLM +zAYfawwAAABABYJR0MaAAwsJBwMVCggCFgACmwMCHgkioQa/JiskF3ACrIrl3G2k +fAVtIquZBtR9B5UrdcNYAhkBygUnCQIHAgAAAACr2BBIVQJAVjo6YTl3gz7BrkZ7 +CPg2HSb7kFGAeTu0xaP1/5s3YscFXRc+YrJuYW1ykg7jNMmiklWso6AwrIZdCG24 +RGXDYed1pwlDEXfMja5dB7WJ93PNudaYRl9TlTWBzub+2r259mL2yt2Ob26PQVID ++GvvYBktVkb9C+HxFrD4AxrC/0tKuuN/ZDMYC12O1C1pkInZpVT0MB30Qz6xhyNV +5+gceqCIhNLvr4AJAb9EsDb9eLoNihU6f3xBcwPlQDNMrUmO2AuIvC8CWm4sf9Yr +2EhB9ehvjrxyzdMOwp7GLZRf7TFXnvBHbgNGOmHhmvRj+1zaF8/v9B8zt2MGncC8 +GUUYtKApygPhzopV2rPIKwm2A0O5BBH5VpQlxczcgm4AhnvrkKJQ+9nDZALc32nj +2/QkB1DK1ltfJmGXrR+CUhZa66MmqHZp7aXU9mlgPVO8sKB7BBXA+VkNuZJzMnN3 +MjNBTf8UrhJrNtcuxxvpXX5XddfgKjkCn+11pc0cdwMc8lcSZJ7I9d34lxkPJVDk +pw+r2rUzkS2Bvw5xU3odS3WtTtS5SPDuZ10zI+xfajOdQhrdkTI8ylvnTNNHYEq5 +wRgsyzZnBntlt1jPwceXh4/bsfJpgCV4Qq5ODV2owijWj2USQw9F4HymvrTX0F8E +FBZ2HP9hdk1dINqq84RGRZR3WCR3wcs5bW89EIUi8fGMUZGXGPlfZDvHf+ay7tDj +f8ltO4j9n36WPDWmKVc7WmVN+YWyZ+E94o/J17yHvbxmuhh0o+XMzf04oguwlCKb +j36vY6+KtUSE8hF9YQFxxGTh/Wz89sTFsNdC6b2GeFk9TCwaXxOmj4KdwP+OjiY3 +/Twj0SuyMYonvDvbCFs9JfZW5eQDDGNati9cCq7f9EQhlTRnoNvi3e3efFEWCBl8 +XjOwj6qI/JKk/i+TiudaX5BsYetuWODDRdGeOXV5pMIg2vaOb/VATS78+0/NVe9o +34wbs9waLScCEMbVWqhf4RhhoJS5UHdJDluA5BM+ECR+Eh5BUSgwfJJ7ehXXV+lS +3RMpfbEj4pZZrZnrB+WvPUOdF/FyXDEriEFrB3JSONtfRyZq1fA+GL/Q1MsTKOff +0uEKAyT+xgYgW79KLQyGCBzOj2JvJtBuyIU0/0i3CLeu4Qvwl46t3/8JQV/uJ8mj +pwfESwmmTtFC+Ndezsr21BhwhwmPlUEt998aG65MnpmWWmiUxqyLOBgDomwJph0z +giEMS/JHk9zGp9XTGga5TdLwSRTRl6Rje9koXHoGC9lokpbIzoqcS7wyQ3RVv7YU +77n9YX67Iol+874za/OmKcNdzirZxoNJGeI2ZH6/+19jfNNfXU0dg8Kx3sY6wlpd +dPXG8TgPtPRJXtLtEOPg4E34k+QYNZiGFU4S1Go7rpl6oVUjq9Gxp9v3yqDHC8Xw +BDssxEPB271vckpScV3sLR95OZ/i91mQg00XOtVHIt3IiTt1w+LSo+Gb7JfNZ/x6 +B86BD9XB+S5pEvaS3rBO315t/Uo3tXdbBA5weEav/JOBiLSRvExivBrUI3EWSywu +R1J4ckpCLo9YajbVGWAU/iZgow04XIg/FKdNl5GeS444JlIAdZoiURrpiADjFtcW +0FTxRokZ81NY3zypOHvrYW2Ke2SmQ7MN4CgsEWgK/qOqzzIT6ZCKJroCjYeY5gnH +WJhpNsPFwA0KuznpRZ3J+TPSoTtDCqCqTZhRGOP2YFbChp2ZX5ns2I7ypa6P7Hjg +6BbM1WVdtW0vhWpIX29QKeCXLNgnaehZyVaHK664Go1rAlCx2ul1zTF0kecHUstd +bYGtH3xA7TO86GohZOOL2N0Ncd+1fMR77QsXonIibwY9MDBS1F0DE7UGlww/8xB9 +3+R74CE7NfYjJmEv8CK9t9wVJHc7xgxoRyRl4QAOGL7KtKYWGKXpSodSLfRES1Wu +j6R2xucdP7M8BhKWjG/rDfuFLlt5wFE7xSlYP1RImq1NEncGOGUHmgNjfekX+PEj +9xBltoG/dJtvVqiXE6CRSCDqxc81YyUkGftIH5gs5IBDmwK3aHWclnbzpA3VpXpX +MYjQgiBwmlcL/uPF+m+tMiZW2ovb5Fm9rfyumILpF/2LLuDCSg72mTfsITmXQ+91 +lL1MM7DdrGVzUKjVKkOPK8Ek9Zaf5b32xOwTd2ilYbK4nbxS8wffGb+ZM/2VsnP7 +kIF8wtP5gA0b3OWP3PsXEkEfa36HD+hA3rdBACCJ5G4roPjE7mR2Pshbxjs24uIw +24J4UFs8JU2ajR0f6duAdngvxnjSH0DXZifTp8EVZJlX1karPDaNhnrIHbkVoI49 +kLcFM7IIX2A0txf8ezJjT8OjKgtwUh7IB0sQ+oM/Lt05wtTNUg0JVjRfRNmkQC8v +97mkMG3c2kHMClhQphUJDuZILeJn82UBrQvaZrxKUfGWUOUbri3cgX5f+ZgVzmm+ +CIIsn5Bvfy2KTGsSNZnD+0/fmNYQ4bwMKBay4vCUnpMzBJoRLJ7eWaNOmYB+KQi6 +rPSuQnJjKhbsZbmPiGA/E7T9VFZubNRq3mzZLu8IVazlEPap7kwzMJMcV3SOfIhU +EvhUJqZ9xOIBtdB/NTMeXdV26nYosLYg2S16PZzaksW4MeO6Vj6W1kKJr2z5xGCy +oRdv5nry9s2OI9U6k7aehixEV5sL1TTbh9QpS0F7K9UmFmeSxkpnfe4jNGQL2nMH +XdHFV/1uGkKX0Cd62wjLjWgKEiQkO5RDvKrZOCCx3V0QtGIb9GV8cXMM2C5EPws3 +vBoOJjgVcQK5+Pw6ZZtGiqnJzWN7HAkU3fB9ud5yYunJ/dGTv+FYRVagnxdCQ+T1 +ieuv/AhV3/NT1jM9UlUH8r7W7VT1pzmL3eRvUrWgOdESaCsiaVt8SO74voSpg06+ +mDI9scvwZyftSXJlBQJECTZywa7Vv0h37hfzYtc0L/COXdkr2DblBbSpvuISRDQN +UMlWmB2eL6f5w86SG2kK8P27tvzUbPHPgPMjkkGEBxVwMTIsUZWck+kKLwvM8eaM +oRldUjMXkie6Dsjjzx8A1IS21EP5yNhVhv5AXOMxYjjoXi6YgqYxjy/5H0s4K9zU +WlqtratBqgSTJ8L1Etd5r3OT+jpUQA3g+ROc+PfLjBhSpdL6gOQsoWEM5ZChrs4c +FCATXGn2DPp7nA+RqDkmP4Ru+su4VYuvsQOTH2soetLnZnepmSB6/x/iA7CggziS +n0yjz+C+8WScj/IR7bdGR1ql2lot4uzk1NTFlo9W/2lczmL2QwFJcB6wj5BnNzKZ +PicgB5kJdvNQehz1W4h2pQNFJZHuyI9WYN0qlreRrz7ybj22V8E49Pxa0c+kSJyp +8ECdZmdo1KgFQhom/ctT1+jyHwEsqycdZfhU2/mp9PyaMoEU1Trh2SjHcC4PzEpt +3rGBoh8xRanKuQvMcfDphcdWlaRYB0NT1WlySouUU6okqryGLg1EBcyFbUZY4JiS +Z+fHn/TB75TgShxLtatocoLwcxFuHsTQ5uuozCOXCIVG5YRhgjELfptsQkbu2R8l +ObrsnqvZF/eOBGmVLU8DhbaZxNBhNDRCp/pCclD43Ne0ok9+5WQX9RloCFPtdy98 +/TT7Jv2x4df4fZQgBnqsi85784fwpsfcWt/n5hmLQIn7D/MfFZsTWd7xk8kBmnx3 +jLMALS8JlDKa2Sr0v5wN2ANo/r0emIvX9FIeDuWJDMkb7YBkJENvYDyJg+IBmcP4 +mc6JOHtb/Xg4lrC49VZlitjvLX0nblYjeMAnV7O6vqn4bk2aVmgK2IW8OYQRaD3m +3BA+QPa+zk9bir7OOx7/MWhF4aZUm0Kz1P5T0cZotscti14DsiUjD7xcRVmQcNbx +cyRxlU4Fspykn7mCzOlwZHlelE1/0PQqwP0xfJga1L0w181qilkXnIVXPSb3B7u+ +oA3udkFU0gchZdg2yMpp5BKnI7CfSWaT0v3JvR1EdXCv7TSgarFaAzwjkOUmokJQ +DTlGeB0eG1vWbt3wHg4/RhzSFhAGUNSmwGAXzrrqw7WoEzvdX19p1+jc7cKBpksY +8hXXTiOEBoKGhyppfjRGVvaEmsN4KoXCXygnvzKnXum+bVjZhNAf6nV15ih2b1Dd +6Z+6QDvgz/NegzGieYhqkmtmf7Wk772VmhZb6xhZhZeXMaTtheNBdejvsX2VflWd +rMc22OTCfZFtwqsVrc3Gl8zJui/bTXd6gp8zbGK3sHjRKkwHJWGZ4vod4sVqpeIe +oGbcozm8w5Is9cNJfiJ1e1w2APOXFoO87nn4OhbVzlmyVpkS8rsnu2BQ02h6XRMR +kX6ykDsG6ucHJGz97eQgpi7m3sha8w7T0Mfk8G9t/dqI3lXz7bYlf+E919oD/CJt +lYyXKu+b8CgQ3Kt3Cjp9No4hpMuDsb18qTwTmZvNrJr+BNQ9f2N+N/8GRW3puXp6 +AR1fYaep6ApUoqu1vtLeDCMoOE2Aj5mauNDuNozF/0xdYZvN8iZCWmR0AAAAAAAA +AAAAAAAAAAcPGx8lKs0uUFFDIHVzZXIgKFRlc3QgS2V5KSA8cHFjLXRlc3Qta2V5 +QGV4YW1wbGUuY29tPsLMuAYTawwAAAAsBYJR0MaAAhkBIqEGvyYrJBdwAqyK5dxt +pHwFbSKrmQbUfQeVK3XDWAIZAcoAAAAA8mMQofa9sLE6dXPrKjih8a/Y8ALovVjr +fk7oFJe9cKiNajyDJwqoAaU1SmujHEKWaQNrdTocTHTaIVr5ut/JXq4v+fMQ3Tt3 +rX4r0nVkOFoBtQA4ToOJsq2K2D2Q9r1u/VXgtI513evaqjclHgg811L5Kdvujvsz +sdsDvOJZKsF27G0XuYa6Igm3EQM223qF5gAgTFS3Vw7TZQ7ynWvvFxBeofbHjod/ +8HpqmL8nWP/i4S0/vzCQboq00Tr0bD01F7Y9Mq8oWneNrKrYjqmSN/kq9X0bXfP0 +y/HJGOdMBFNxVyo03AWG9C/i3WVFdEnF5iaQwUoudt1l4F1JYBRQoGh/LV0/rMk6 +TXWLJyh8NFnZ6qlYF56O4ZaF2OkIoWKWvJsFI0sHzfW/uqpbp3k2kq7WQKeCp8N/ +UYI/l34l4+w+TZpFnzPO+rKS/JYu96eatCsfvIrD2Xg3hfwd0tYg+r+JWRpRU7aU +eQAnFnlnpyIbnnv74Z27q7MzmF1/IPrHMvEQccnswHD3ND9QjmHstEC7Xs8i8CzE +IiuhMQLLH3DYYBtKMJBd2H0/vNqacKmucEek5K0lFxWO8Li0N8J+s5QgFIP6DiEw +hoyGo/oLrMAufqZswevWEtNGpX320yOc1flAfnbPUOv8//71mpWUDdHu13tjDZu4 +nI90VEYpqbGKEd6lDEWE68Hje4Rl7HMLY7WMhozGq4F2Kzk2q++120lDaRW9IERj +DoqDduMLRvpeahMafl8ERAesdW8U7P1NnNkOcX69AcLYbtO/pAOV3HPwxHRLMyQ/ +tY+jsp0XA0VZW+ih8qwI3cs1yhafCl4txiekPaFfpiS1Aqv2zTWjfT8gIDuGInYf +OqnhThcPqDYKf4TxF9TdkmZDNdUlyGs3I3ZwPfW83e4oH9Bv3+JDgMYf+vCZioo2 +Z9trp1ENrR2M8qHsVxuAA7XwjqLfZQHVTfXCDdhJm0KaiQKCwSx+f/tcjvzXKPQP +Lrj+JfA9q87TZ8bukt3AnmeYux6CEbHI3IH3YZYJESw/wEUC20Q/0ezrhen4+nBj +Ff5mCtW9+/lszcyL1JTwjcGYBKIwnPcdv6i64QfnVZE4pVlIl5wuF+CQZ6RW/IV2 +qA/euV2J1oER6CQmE6DF6r1xyTwNTDJaF5XGHqQUzQnx687MlBCbB1DkeRVOnlcm +BAy8NXra3ezs5t3V8tb5DaEG7o4PLWqHt5kp+7cMRseXNL8c/XOqRAkqmFCojDQA +A7XX4j9V3XU0642oHpOuNbpSZdYUza/daomkOZgi7PpaVVQH/ZxdwI7T+M+g9pNy +4q5n1WgbUoUGJ746tZB/N4OFfkJ0igbsTotGH0+ypPj/1MzwsjlowYdqMgYIeyLc +6GxH703CBLSufypmOZOqu+ovrjoljgLhnYGe6T/GkSaqOQCiTTFBk1YBf04Ynsgg +RHROEPUu9u+ObmP+ODhx2MlK9lz2J9fBHygmx/dgoWQftmV9YqDd5T+iWsii+74R +oUQhUSNaMOp/M/k4Q00HZn/XaVivCWVJ0PGEav/lV507kqcrL02t8kl3KQKjZRsZ +AANlwotWU0VVLQyE7izkbmf/Wn6Z3aIdtdlbXQa3mgtHikVgz2LUvDkE/jZFVd8d +Km9HgVMnTSzj+deFN3nGN1y4pP/u01D7CWAg0xGR79aEBybUdEcPLGLSUSnJrOh6 +6vRkwngYdXgjBT+fOpM3i5JMu3kR2EW784gIIeZ5v/vMAkO8mpK5io0kZP1TXMVI +DVoaXqdGWG2Sr/Q4P1nY3DSkAjZ27T8XtcD0RF/FgxNkVTraLof9ubj6cn1TBBy9 +1MvHuXtpExjerqXsVmoimqvOEr4DNNh/IffVc0Mb0GutSeLi8f5ApDmSAIbMPmKK +vAtNg0j3uHBPdDSQkAltq7RrPZwa+MQQvC53oYRx9z8voEKNLx+vMD14VgrcGKtS +4qmmSurQGWYDUMWxQzPHZe34AC/ee7ty9ACvqDGkVLkN5ggZkXKs07/8v1AJMotd +6T6ubXU7jQZW/7O0CO5WhzsNxdDee20YOsCIaN029DIQiYtzCdn8IP/pNSJLuAS8 +F6i13jNpZojifpu0/dX6peQW6N+gNHOT1G1OojXtL0Q0kWcI/E6Y72ja1yGq22eT +t8Yc/dldqUa1f63KX7TkLnX9RWsiv/cVYK9Kh2K0tExSowR6LSoURED9LEK/bwkT +14JeZ8PtcFxaCZdO165sbfHJaum23GRds2oF/UepXPfSxXVd4o8fPhUbMie6El6z +/OJKf4uxhMMJxOcBp2gAzWksQeEl0YwdNlYKXXrozPzFFZ0eczwclmuIBAJTSup9 +JQ+sye6FQpac6LIFi2Rw937rlcJlaM3d/QII2fsDldndEADrGtlFOx7P4y04BAzq ++eDehpsKE8nA0jEYG6bcfn+LDlZayv4+NDjEb6o7JkQDDZ1K+05cH97P7m4OoGsr +EZILNDqp4hnWmiwoRXQYRdW5QTM/IGOHdn6BcfuFMsRIj+9ArUnxgp0BRc67AXAE +sAtjFApY4x4Yf+echH3xNtsoSMFitCK0HjZ9cV3QNPu5R3wN/vMa/qE9tuW714vk +Ws7F/viCpVxNe2fVQIuCbkBwbnuB/gf2UDcmyiIjxtdpbY/KVm1ga1n/PeV1KF1B +35gjpWIuQRitY6Il9sTm3LLUynrDM9xWLTMVbWpWqkJzN0wvIfOR3yyTKy+lgago +8lKNYSJ5gyHMzwey5dJUxnqMzi+ffDCfgDfHn4aXXApWRskRXCRc/rR55aCSco27 +izDAxnzuB0v6+f24QmdiqNdLuuT9vx/1uD4O732i/KcJKP3LBiNU2opO1Z7pqT78 +HwJYtUpeAS8mVr7KcVV+J70TvnmZocjKri2MDAGqwEPRm5IKkoPW0uvElnNBqMd7 +pC13uscqjzWap2alff5yCbltIlySLZkPumvDBAwExFqqvMbtbdGAS+dRkDI4mvo0 +EgMAmoo1S2PhRICN47shJftkJasQpQg+eNBwJDMWTsfbD100S1Of05z/nMj2+T9g +ChufB6jxKqskAkvLCkqrZvI2QwN8G7tnWqNc9j2r+F37L1DhW6WoAdC9AHnu+CG8 +ml2FHrCNLWgIpFytFiqXgjO6Ae7etUJ2+WPfcpfoyqxr/WHCo13qU8l0VUN66JC9 +GB0DIX9Kl8Gh96F+m+RJ/4iCZJsh5BYjVAOVEvYuWUkeaTqS1knc5NQhny209f/A +sXwfw/N4f5gj7Lr+pervckr2EgvSbP8qwQ0Dv+BqMFTGorxNYJ0qpALbeXGR0sIS +JnT4gRWE/xdl2Zv/CdFHUi/Ab5uM8cGfMKneu1s0a+sAbe+acGYVplUVQNBKY5uP +3jise3m+4sKO5LNzFYfvLNTEu2cFDbrdB93jf8odU4PJxb095o9zvuvCriSyX9VT ++brGlSvse1VZQUt88egDAEoYtM5ByCS/Cuf7wpuiKHU4yZnhw+1y8vld1UHcbCSQ +WlNbGTtU6dTvQi4L66fqIJrUumZHPTqkRi9OJchOMzQYTZAts29AiQqwadJSXB80 +Aou0sso/iMkJkxSDJduw9mhu4lvcIFfAb2k91liqL33uqm1QGJcQqfKdXKmpomlq +Py3n1Uio5JsfJOysifkSm2fECD439BvBkpzoaX/IPcKHI6Id+V/9j9uecycp9wOz +iQDN47Z8QHwkzsOKpvy8bAW0NKjFzLI/1A0Bv8Fp5e5YEiEuxSLaloOxA5KMV5Ur +Ej7y4jRqXbS8efZtX5pQCyNOH77ksYc8lkSnhoXOI79P0vACXH0TYMap3DzA5Lww +3k6Cz+ByrHmAlTDrPFv5/T92QUBCiRB5sf8borzdMCVei+eaXkFpnbcQsu3YvugO +3uJ6y1fEunVpwoICfeK5vboULxXoe3VQUNSc/VZpoYXgJDLZf5TUwfoTGVnsoZzy +MLa/TYRd+dMVNhyqEiV0EmVbtEoWt1Q1+imUreisnJlETbADk+hgldh6KjPJGLQt +Rvx8frAQE1GdFAONktroxfi/AYGtpptyCjeSmMgEnfzsKyujZXflZTsD+y9yZ5ni +yyTBAMZtBlkfOdQ9Pc5IhprYADoeA30tkrh2u5z7S/JOcTdIx7dwgNT7Zpdd/4Ro +emprhIv7GO9Idcu6M9mLi435R6XpyG85HETbbZJSFjq3Wdo0/DRvbuwXQBJSXgww +jCLKiLNZ6TatXiqpIS0JO+AAVV5rjmMvpS/1xKsPwHMSc7fqxlB1Eze6OuFdv0ul +872hwzEHZijO+nLd5wO+3tAjTOB/fsc/KV9DYZIUhdq50Nv298w/L98072XCny0C +cefse0Bc7a8EqSB/ui2L++sBQ+ndW1CuT2hyUzRHptcN/UR58O9Opfdr40v2B20Z +/L6rYlVbkS3iSLHi8/uapIVS9+PpF4jF5Go6bK9MhqssX0V1LrU3Lr8HlCguN2qw +uvL+C0p0fovI4/wYJ0eMn7XGzOb+C4fHydIDChMaJTJpkJLz/QpGUJqnAAAAAAAA +AAAIEBofKi/HxGsGUdDGgGkAAATALm+keAeD/LTaHOQ5Dr5kf367WyONEOuDgR+a +FIB6iB0KoFoPARQRW4rcyKmN+Qp/oJgEOFtPcKtVbC8WUCJqoxNmbKSFwZiBs14b +1glanJAVxriiNrpm2S7UxQd+PJHj8nWPvB1jTDpi4FiIjDndRUgFJ6H9KKN2miDA +Cs3f4BZ4OM7m8yNbtgSlRTd5YU1qSb4MlqmR27D0p8CYSX5l6AcjmVf8+1dO2Eti +Gz4aQBF/SZR1YbB81r+XKJNGIp4hsSr0UiNxQQyYDBpdZ0i82sEnZ2yDQiRAMT3I +uTDoAWEjeh5nSkXKOGT9cMOidbZZGn1gJLyTLD05JIHJ5Ip30J4FpyGcw6t4G5Sa +9gHHBSJawrYXh5oyBh2mKGLLq8u3s2CMMo52Oqw+EQBh4ULLoS/lQn+Y5jTT2puq +YYnJymyAvIumSozb51hYWBNxjKfDoB3mYBATNwWzaQvYEhMdpL8BqgPdcIvVxh1s +pGoqA4/ObK6HNnJseSgcELaNxBjjOjWVxKtmuCo6w1uNoycCAMn6MX7UC8uhARJ1 +WIxA1Lc08mbkw7CFkgBXwigBCD58t4q94znqxICNoWpWqxWhYJypgx2oeibSVizf +S5pvLKW6ZBwAa6AYFYoigJT8Q6PHVnM/EC135U/8tIJRQECcwW6z+8vZ/MQfsEVP +ZFqwpRlcmxDFAnPzlwW6pl07W8nPlUhrFYOw5UlrAgQtpSgNhoBW4mDSV0kHSQu8 +4kbCHDrbyCBJw2BUK7a4iamNlUUyAmRjh3omhYskwad4mSbI22LiKjYEGZpPKEun +KIAZ65cPAgo5JczjiV8t52hzyz4B+V/AWB1JvFhFa4kLg4Xg9ZB2ui61IBGWNVQs +oRz9ChY9hJhX6E8D/HJeSjcsqjuUqSedmXmzonYu8ptvanoJym9gprPK7FBeWBkG +ZWaRSxd+pUPQ4IVdYTEg9Cw8dDXQVIrTMo95lmHxFQcBAkPVAQVIlcUALG99nDSz +gUzJAi2fQcgXaiAVwaaPR0AcKE9Wt71GIiHKgnLoGoAQOc9JEQFUZoO1Q158aIIs +d6ZiI3a5S8bGrMhdhoaR8GPr5qHsqTApi5Qm1ocCuTklFDbDBqdeNpCgVCofDHE+ +gnhctC29t7lSgsHNbFEKhBEe5WfhpoVwQ08YGnlqJpjfIVC4VJx+xoSL6HoWUKj2 +CBNtIrkPa06DCVTGRmOTlL4sd7iFChTpkB/0d0vOw2vdlQvIJGKCK8f2iC5F2hef +E7YJx0RM+7daBB8Dc4ftCBckNofw0aP3+WoXQmc7UADUqRPXm5m2k5hILMzkYmmq +AJbpBEMWUItKLGtfM3l9+XHujDZhUSc0hgATaC9Vo5G4GKdDlCHBQXNYeF3dinNt +IB9Btwo2qRqKsLHrinoAAkxFuix3+0ryiaJMBLwkiRF3+Qi36sMqC1iRZAVFRHKO +tWS6ebAjpKtAwjDVKwEjIWUglanQJ3iRCzIJQwH1FmCwi33VgC/AlnhI0a4ACY2j +I7jhkjn+ccD6kSanp33de62/1w83ingCkx1ftBrEsJzepj4L3MwyxaT18LuR81T8 +4Y19mJ7EszZOb7nsnFnQj/dzmL4JOR4KpVNV6+9pZFtRMXoQfACjmJi64pUKiycj +tUOzSOI27vETDYGQY0mDttOFsgnTy8jxh4rx5bvB2YX6gACq/xRyB4vHm56nX/BI +2g7ax9dmg/pSyGigD+RbsAZDBvxpYcEr4242od9YmLnNMyHypWXCzLgGGGsMAAAA +LAWCUdDGgAKbDCKhBr8mKyQXcAKsiuXcbaR8BW0iq5kG1H0HlSt1w1gCGQHKAAAA +AE4REJBeK4w6BgxSX5H9c9AQu2CHzklqkYzjcuYVYiTqZfmGpwudE3u171PNzppo +BZv3NUVHJdEoEeHhg8bGwpi+zbAxv6foQqp1+YvPeZqcKjAPwODlB5pXM0fF33GZ +m6dNaGKq07MnAaKeGMp5N5HZLDyfc4TdCk3oCfJrwZ1aEAEsDmKwtrujdqJWdgzl +XsvMJMRE9im5GuadIIbkNIMF56GnQvzmZsLZFZ5SYEfyn1RqCTC8LvLmLUv3PiPo +mURReIXT77FZy13FHczLjZMWMsnPwHsnFcqRsmgx27vKBdKSUXKPI7w0wte9zUNE +HvCN95xc32CLI1pIjFoYq/5VXLrdjoo2CDR8POamYnrmQGhVrxOI/RDJS5PTB9Zb ++QHgqKY9DEOG/2VT64kn0eiw40E2PZdTF8dS6KA+arWKdiiBwF3iHzaGAUX0EoJ6 +BFxgyXF8G15ABZL7G1is96OhpDpWzlfVgZGxAJq2jFY870QrD39/EocfSpUP0kRb +IL2a6xt3omwPhPPfDUVNnMug9n0km10ITZ6+gjN5PzTFqnI1w6+uCcjzwH//9y6W +1fUgY+Cl8rxCskvZkDEWMkIhRVSsLK4Gzb6yhvLH0cG02ikJuv1ERYagtAVi9pOP +liFlX3QNvIWopfKhoDsHjB0OM3TlqREdTH2J9YlviJ96uyfpp4h9qdBj+9Z7uCcx +z2nE5YNsh2cKGMawCPGjWJMncsOVkPmc7k7oe0fSwh4dECAaiLysJaP7WtfoaT+6 +8afqcmnm0oMf6+w2Kvh4gpUnjMaj7QRT5cmg5Oem0UMEtAQnNCcAH1ButBAbsGu4 +V38R3C/TS82mowQ/VkxQG6tpnNdFOgFOWYjxizoAXHdZxnGTKfzjJU3sZSumZWBV +KKfa8PIkMbh1re85MrXNKhrjS+Du3ZElz0fygmx3kcfEOGPuI9qq3T/x31zVfB2v +XnXDgW4GQqyGsIj+KAw1/nplMYn9KkSYVapu2MZ3s6m2X6pdo8NFAFyO4h2r+UdI +Hr03KZ5mXj62Bk+391vPIGowWnv2ENNeK4tBpZUY+ZP56OPY97ZvZhuoFqec9JkC +Dr3bZeQsf3qrFG7hjiOTNUoUjUjYTiChSRevydJuE4W9x24ddB+oBKr4zBTjStZz +rJ9GKpdxxCT3SkMrx95mOsuIsNDSUgCZXGuVXHLCXmhi1hh+AoJcGc117Oh0J1VI +cvYBQzvLSMC0t5ZAKB2XcDqVI8ocUdsXA/v7Z4EAESDi21Ln7Cw2Rh/+0GkrADwv +7srrYuYQK3/1OqUvL1MmKvCZ5QUoqq+KMzCXF7Zoum4IGf701Hba+8bOhN/6Ohf2 +BkDdrYe1bieG8yvsGSzXdy9wSHt0dfAV6KVKgk+mbciFBKbvplLstMmoagC+IKUI +F6OA2uQhsZrnVbq/mTc5G5RD/8YFe5kj/iE8bhdQnRTdiaCsYW0rrfgTle4g5F3x +VKXLTaC52NK4y8sZAEXHm+EPXo+odH24XDh6nCNbkXRjq6R9pm7/LmRe8NV6nnV0 +yMGCzEqvEIe3UyQnauGemwwrYAT4CzYgBVjnqESomwg35+439aZomHLiAnSA94Ae +QqON0/nvrCs8i4WChmDKk/5AZeaSsJCKliKYq88tv7uklCvMK/M0vYw8f3g5l+iW +TzjM3mhs+VZ3sA/WlXXc/b6wc6aQRH2gyuaXvoAqRzZBGmlYKd8XvGR7NeY2gp9m +wVZ4vT/WbQUzD1PowtI5uPN6UNPL4HpzYRYnIR0pLNozIY9S0/PT20W4lTv63shs +0LxnjOZQBHZuriaGL9Y3tRjKja58uQYjfHdkcMdu24CnKglZRykzYjmriU7ixSOd +J0ezTeoA/ECBG/Z+rXBQqVRd+qXfqUSildqmDZ26dD9IQf+Iydku3o5tBwl8WjmD +zILR2XOrDhj7B9Pz0DFgynxfAM8BZpH8G+z7jLswm7LCCAliHLb5pQmPwqlbl+CE +2Qlc2grfp0T3H5pwdcxymaMCsXl3DF9ZoNwPCbozckQ9Qf6RpTC8Nm20r0wQlTF4 +LQuN409ZHGohdOUzCDW+6XYvW/ykzsZFYBuOPmjvLp55hEk5LmT9sQ2jxJIX3/zn +kGDD8hFWNpgxsaDqBSn8i+M10locMkaKys56dd5gr/unObrFMsWEE6nB8Rep7V9j +ao9Q+NpT8e2QbqQlMwuErxUdkOyIFi0hp1zFNybEe/xwq3WriYBsS+MCEb9PesaH +Kwa3JcpaskaCSdnjEUjIhSI2HAkIfK6uLdZZ5tU3W3Xi03qdqB9i+xYKkCsA0ED0 +ZAVh/z4rw0KSaws2TogUyFkwfC5+ie/aJAQd2Vnh6L1ZM5D3rxb7HizIGrHvpYgv +gEiSLmB5Md2ALxLA9PX8mWz6O6/dJt2aUUzbRVCO50CnhPt9yktdx4m1JnKozvW/ +rKv4SwJ4T8omIpnr1g870Wn9JtCfaRjGqhVLUqVQqC13AHLTjRBJqeJCtp3gaJSg +as5MchsLOww5kOZoeClTUSCrWRSlk85RFKrq1hmkVBOvnsytaytS5U/lcuamG2Cw +1KFsipmwJH/FLCjxUiHkbsdh1ZCAxQzjhvMkUCnPI7rJBhWZPBlM1KyqytOvMPTN +UcPA9AFj1Ks0UqABgLSWSoyfiFm2FAZ/fDC4qDZmRkfrRnOvLOt2eVlKri6hKu2a +g+MHUJbZilj65ryRC9RxmIs3jfWGr6UxH9Sim4kTxEWhfWccvNx4G/0yPyuDcKmj +AXF1uqMZEte6a42S86LN/5MaerGMqgk4iT6hDa3LPA2WOsv+lNaWESVOYT4jHZNB +UFhgRJzDre+vgniqyJpZBzWcYMY8zcQz7PiCOXPm+XNLIr2tKeZAGuC4uiHqPSG4 +RGXtK05GG0af7VofZbSsySp/hJ8RAAt+sAi1XlSpE4G7th6SSeMvrQwndkPO7N/8 +JCTlVCrm7MvZEw7uPs8wN+QzTyXcuMY7dtSBcXPrSJZFdRloCV2ueVgfGfX5qfL3 +xz5yjlWuDookqGrgT8gvN7dUbn5hLltGy0EZZRkg+6jhqS+LLSoNmWHnU2ndzyjh +ntIknmAstlLsgUEljBDELOdjioelnauM57gIajtMk45hXyxtvmX9ElEdZG4nA7zL +StEHUGX73QGwV3IY0DPunz23ztrctmku8az6krOmcFjTLi4OzETxe5ypqsUsc8LZ +95IqKKCZYUdGCQfBm0PVPlapCi3rLh2uM5nxzwvZLYX2SeAoMxS8YckUEtkLsinA +uVAI4ESYCgQqt46x1HsE4r14Y10FDfPiJVLvlKKQzPiZD3LJ1WNWMbinubcoorBQ +WQF+ir1fPmr1kdbvtjVbXp2kM+HY894swnUwOfHF8fM1vCCbSbWDk9J3psvhrcF5 +dDiMn2Jx0UiU+JbQDhm2O9hyyBVUh49yfU5m3TLuOfSxDo3MaYUgHHZ4XgQpSDbd +Degs/JINxTlqU71jP51MLf1sxu46EiurJIqY5nOuIBgq6WXISjaImkQxXuRgbM0Q +e9GRnOMCv6ldEXz56XM9SscmqsaaZVeQNaSrbqRwJONpIzYmXdMkVBklq7Hn0SyA +Hi+BIzsuBq9O19y85y9xzqVwDGUz8IZ5kRNGuO5mgq/IEDEMeEntLKmKDkv0lDCk +3c6QYvXtGsvn0A8l2sFDynnZBJCA8X6PJtJhKSlHUx44oFEwLd7IfBlNHXEAeMcA +YLcBv4n632kvOc6DkP9gKI80k/nHh9OJJ/Pf8ymleXC52Ah91PkcXtDqISdxZtRG +WsE9Ic1JcjiI1Dwc4gLn3DiTGk93oLwbMHOBVwXxnusqbprY9WzajBr+Rt4cbH9G +0Gev4AUUA61Mzw00lhxKz1ybGSdmzp6ohAljkPba8X7H4Q3JNsQ9Npb7DUsfjHxQ +j4qd2MaQmhFAeRfEUSOQSj7rADgYkkjkmCZr2kzUYowpWhe20jzfb5CGW/eNrjiZ +dzWw3ViENke1BKrrrAwmTrU4gFjC3K7SBBarOBDzFZ80BmcdXrzFoLVr5Lbvjs9E +MDxdzLdwEtslxdN92J9q1BsnW1a+uJY72mL84R5dL4ywVF+TcJdhvs5fZzIr1+P0 +8JAdBUWEXdm7IoMHEAONPzscm0EZ9RW8MGgaCa+gwS4XjLEh7kSN/LQC0DXDUS+0 +9gbJlVVbte3TaHSpZTt7Kl6t9KtXHlPXU6sl3ursFByqzcqnLSdd46NjJVqQB+Tj +iKglpWIn9x+jMgb+bkKK/dwyeTaWZ7FCkSIDmfnKe2+tiDs+0ZqbbgaHYfLpWe+y +8u7GfakRAehUKedjRFvjCqKIs2tlR4r8Pc0coNAAfasPRc2iCIOl/E6Q8Psp0MDI +R7or8z/OgjUEafVEBVZML9/zu4djz5Ql0U7RxmOoFnZ+TC1rnnozQBcBNVgFntlM +3dC/5wjTzH0K/cDIofaYbadcWmFbdK/B+gwukZuv6/ADssjK6jFXXnCtvOghLjtO +VKDo/QsSLzhDpNsAAAAAAAAAAAAAAAAAAAAABQwRGCAn -----END PGP PRIVATE KEY BLOCK-----` const v6Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- -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 +AAAAAAAAAAAABQwRGCAn -----END PGP PUBLIC KEY BLOCK-----` const v6Ed25519Mlkem768X25519PrivateMessageTestVector = `-----BEGIN PGP MESSAGE----- -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 +jXoyrGhDDakI/1XygauU5DpNuhgApoA= -----END PGP MESSAGE-----` From a8180e457dfa486fe4b065068b95d78b7fd094f7 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Mon, 20 Jan 2025 14:12:56 +0100 Subject: [PATCH 12/14] feat(pqc): Add SLH-DSA pqc signing algorithm Implements SLH-DSA with circl according to: https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html --- go.mod | 2 + go.sum | 4 +- openpgp/integration_tests/v2/utils_test.go | 14 +- openpgp/key_generation.go | 14 +- openpgp/packet/packet.go | 14 +- openpgp/packet/private_key.go | 40 +++ openpgp/packet/public_key.go | 84 ++++- openpgp/packet/signature.go | 39 ++- openpgp/slhdsa/slhdsa.go | 73 +++++ openpgp/slhdsa/slhdsa_test.go | 87 +++++ openpgp/v2/key_generation.go | 15 +- openpgp/v2/read_test.go | 14 + openpgp/v2/read_write_test_data.go | 361 +++++++++++++++++++++ openpgp/v2/write.go | 4 +- 14 files changed, 744 insertions(+), 21 deletions(-) create mode 100644 openpgp/slhdsa/slhdsa.go create mode 100644 openpgp/slhdsa/slhdsa_test.go diff --git a/go.mod b/go.mod index 25880a800..e59aeab28 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,5 @@ require ( ) require golang.org/x/sys v0.22.0 // indirect + +replace github.com/cloudflare/circl v1.5.0 => github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93 diff --git a/go.sum b/go.sum index 1a97c0f33..dad91470d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= -github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93 h1:lLX4wx3iE1uDt6v7pjcl2P8z4xTFHsp/1wOTRO+NPfg= +github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index ef9c18bff..bbb7bf003 100644 --- a/openpgp/integration_tests/v2/utils_test.go +++ b/openpgp/integration_tests/v2/utils_test.go @@ -30,11 +30,12 @@ func generateFreshTestVectors(num int) (vectors []testVector, err error) { v = "v6" } pkAlgoNames := map[packet.PublicKeyAlgorithm]string{ - packet.PubKeyAlgoRSA: "rsa_" + v, - packet.PubKeyAlgoEdDSA: "EdDSA_" + v, - packet.PubKeyAlgoEd25519: "ed25519_" + v, - packet.PubKeyAlgoEd448: "ed448_" + v, - packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v, + packet.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v, + packet.PubKeyAlgoSlhdsaShake128s: "slhdsa128s_" + v, } newVector := testVector{ @@ -240,6 +241,7 @@ func randConfig() *packet.Config { packet.PubKeyAlgoEd25519, packet.PubKeyAlgoEd448, packet.PubKeyAlgoMldsa65Ed25519, + packet.PubKeyAlgoSlhdsaShake128s, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -270,7 +272,7 @@ func randConfig() *packet.Config { compConf := &packet.CompressionConfig{Level: level} var v6 bool - if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 { + if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 || pkAlgo == packet.PubKeyAlgoSlhdsaShake128s { v6 = true } else if mathrand.Int()%2 == 0 { v6 = true diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index df4fab454..1b1387f06 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -24,6 +24,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -340,6 +341,17 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) + case packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSH key") + } + + scheme, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), scheme) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -386,7 +398,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.ExperimentalPubKeyAlgoAEAD: cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) - case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index f65efb2e5..2d68a82e6 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -519,8 +519,11 @@ const ( PubKeyAlgoMlkem1024X448 = 106 // Experimental PQC DSA algorithms - PubKeyAlgoMldsa65Ed25519 = 107 - PubKeyAlgoMldsa87Ed448 = 108 + PubKeyAlgoMldsa65Ed25519 = 107 + PubKeyAlgoMldsa87Ed448 = 108 + PubKeyAlgoSlhdsaShake128s = 109 + PubKeyAlgoSlhdsaShake128f = 110 + PubKeyAlgoSlhdsaShake256s = 111 ) // CanEncrypt returns true if it's possible to encrypt a message to a public @@ -539,7 +542,8 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, - PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, + PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: return true } return false @@ -549,9 +553,9 @@ func (pka PublicKeyAlgorithm) CanSign() bool { // otherwise, it returns the selectedHash. func (pka PublicKeyAlgorithm) HandleSpecificHash(selectedHash crypto.Hash) crypto.Hash { switch pka { - case PubKeyAlgoMldsa65Ed25519: + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f: return crypto.SHA3_256 - case PubKeyAlgoMldsa87Ed448: + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake256s: return crypto.SHA3_512 } return selectedHash diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index aa0bfd61d..05fd909ce 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -31,6 +31,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -174,6 +175,8 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) case *mldsa_eddsa.PrivateKey: pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) + case *slhdsa.PrivateKey: + pk.PublicKey = *NewSlhdsaPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -582,6 +585,18 @@ func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) er return nil } +// serializeSlhDsaPrivateKey serializes a SLH-DSA private key. +func serializeSlhDsaPrivateKey(w io.Writer, priv *slhdsa.PrivateKey) error { + marshalledKey, err := priv.SecretSlhdsa.MarshalBinary() + if err != nil { + return err + } + if _, err := w.Write(marshalledKey); err != nil { + return err + } + return nil +} + // decrypt decrypts an encrypted private key using a decryption key. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -890,6 +905,8 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeMlkemPrivateKey(w, priv) case *mldsa_eddsa.PrivateKey: err = serializeMldsaEddsaPrivateKey(w, priv) + case *slhdsa.PrivateKey: + err = serializeSlhDsaPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -930,6 +947,8 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa_eddsa.MlDsaSeedLen) case PubKeyAlgoMldsa87Ed448: return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa_eddsa.MlDsaSeedLen) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + return pk.parseSlhdsaPrivateKey(data) default: err = errors.StructuralError("unknown private key type") return @@ -1319,6 +1338,27 @@ func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, seedLen int) return nil } +// parseSlhdsaPrivateKey parses a SLH-DSA private key. +func (pk *PrivateKey) parseSlhdsaPrivateKey(data []byte) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 SLH-DSA key") + } + parsedPublicKey := pk.PublicKey.PublicKey.(*slhdsa.PublicKey) + parsedPrivateKey := new(slhdsa.PrivateKey) + parsedPrivateKey.PublicKey = *parsedPublicKey + parsedPrivateKey.SecretSlhdsa, err = parsedPrivateKey.Slhdsa.UnmarshalBinaryPrivateKey(data) + if err != nil { + return goerrors.New("openpgp: failed to unmarshal SLH-DSA key") + } + + if err := slhdsa.Validate(parsedPrivateKey); err != nil { + return err + } + pk.PrivateKey = parsedPrivateKey + + return nil +} + func validateDSAParameters(priv *dsa.PrivateKey) error { p := priv.P // group prime q := priv.Q // subgroup order diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index b96469f07..56dd0958c 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -33,6 +33,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -42,6 +43,7 @@ import ( "github.com/cloudflare/circl/sign" "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/cloudflare/circl/sign/mldsa/mldsa87" + slhdsaCircl "github.com/cloudflare/circl/sign/slhdsa" ) // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. @@ -321,6 +323,23 @@ func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) return pk } +func NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKey { + publicKeyBytes, err := pub.PublicSlhdsa.MarshalBinary() + if err != nil { + panic(err) + } + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + q: encoding.NewOctetArray(publicKeyBytes), + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -383,6 +402,8 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize) case PubKeyAlgoMldsa87Ed448: err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + err = pk.parseSlhDsa(r) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -833,6 +854,29 @@ func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { return } +func (pk *PublicKey) parseSlhDsa(r io.Reader) (err error) { + parsedPublicKey := &slhdsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + } + + if parsedPublicKey.Slhdsa, err = GetSlhdsaSchemeFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + keyLen := parsedPublicKey.Slhdsa.PublicKeySize() + pk.q = encoding.NewEmptyOctetArray(keyLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return err + } + + if parsedPublicKey.PublicSlhdsa, err = parsedPublicKey.Slhdsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = parsedPublicKey + return nil +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -927,6 +971,8 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { PubKeyAlgoMldsa87Ed448: length += uint32(pk.p.EncodedLength()) length += uint32(pk.q.EncodedLength()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + length += uint32(pk.q.EncodedLength()) default: panic("unknown public key algorithm") } @@ -1042,6 +1088,9 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(pk.q.EncodedBytes()) return + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + _, err = w.Write(pk.q.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -1151,6 +1200,18 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("MldsaEddsa verification failure") } return nil + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if (pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake128s || pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake128f) && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake128 requires sha3-256 message hash: has %s", sig.Hash)) + } + if pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake256s && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash)) + } + slhDsaPublicKey := pk.PublicKey.(*slhdsa.PublicKey) + if !slhdsa.Verify(slhDsaPublicKey, hashBytes, sig.SlhdsaSig.Bytes()) { + return errors.SignatureError("MldsaEddsa verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1384,6 +1445,8 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + bitLength = pk.q.BitLength() default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1427,7 +1490,8 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { func (pg *PublicKey) IsPQ() bool { switch pg.PubKeyAlgo { case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, - PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake128s, + PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: return true default: return false @@ -1436,9 +1500,9 @@ func (pg *PublicKey) IsPQ() bool { func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { switch algId { - case PubKeyAlgoMldsa65Ed25519: + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f: return PubKeyAlgoMlkem768X25519, nil - case PubKeyAlgoMldsa87Ed448: + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake256s: return PubKeyAlgoMlkem1024X448, nil default: return 0, goerrors.New("packet: unsupported pq public key algorithm") @@ -1457,6 +1521,20 @@ func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) { } } +// GetSlhdsaSchemeFromAlgID returns the SLH-DSA instance from the matching KEM +func GetSlhdsaSchemeFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { + switch algId { + case PubKeyAlgoSlhdsaShake128s: + return slhdsaCircl.ParamIDSHAKESmall128, nil + case PubKeyAlgoSlhdsaShake128f: + return slhdsaCircl.ParamIDSHAKEFast128, nil + case PubKeyAlgoSlhdsaShake256s: + return slhdsaCircl.ParamIDSHAKESmall256, nil + default: + return nil, goerrors.New("packet: unsupported SLH-DSA public key algorithm") + } +} + // GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { switch algId { diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 6c1ae22dc..7904980a6 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -25,6 +25,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) @@ -207,7 +208,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, - PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, + PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -358,6 +360,10 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { return } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if err = sig.parseSlhdsaSignature(r, sig.PubKeyAlgo); err != nil { + return + } default: panic("unreachable") } @@ -377,6 +383,17 @@ func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (er return } +// parseSlhdsaSignature parses an SLH-DSA signature as specified in +func (sig *Signature) parseSlhdsaSignature(r io.Reader, algID PublicKeyAlgorithm) (err error) { + scheme, err := GetSlhdsaSchemeFromAlgID(algID) + if err != nil { + return err + } + sig.SlhdsaSig = encoding.NewEmptyOctetArray(scheme.SignatureSize()) + _, err = sig.SlhdsaSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 9580, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -1056,6 +1073,22 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sig.MldsaSig = encoding.NewOctetArray(dSig) sig.EdDSASigR = encoding.NewOctetArray(ecSig) } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if sig.Version != 6 { + return errors.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + if (priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake128s || priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake128f) && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake128 requires sha3-256 message hash: has %s", sig.Hash)) + } + if priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake256s && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash)) + } + sk := priv.PrivateKey.(*slhdsa.PrivateKey) + dSig, err := slhdsa.Sign(sk, digest) + + if err == nil { + sig.SlhdsaSig = encoding.NewOctetArray(dSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1199,6 +1232,8 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: sigLength = int(sig.EdDSASigR.EncodedLength()) sigLength += int(sig.MldsaSig.EncodedLength()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + sigLength += int(sig.SlhdsaSig.EncodedLength()) default: panic("impossible") } @@ -1312,6 +1347,8 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.MldsaSig.EncodedBytes()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + _, err = w.Write(sig.SlhdsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/slhdsa/slhdsa.go b/openpgp/slhdsa/slhdsa.go new file mode 100644 index 000000000..4aee70491 --- /dev/null +++ b/openpgp/slhdsa/slhdsa.go @@ -0,0 +1,73 @@ +// Package slhdsa implements SLH-DSA-SHAKE, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html +package slhdsa + +import ( + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/slhdsa" +) + +type PublicKey struct { + AlgId uint8 + Slhdsa sign.Scheme + PublicSlhdsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretSlhdsa sign.PrivateKey +} + +// GenerateKey generates a SLH-DSA key. +func GenerateKey(rand io.Reader, algId uint8, scheme sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Slhdsa = scheme + + keySeed := make([]byte, scheme.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + priv.PublicKey.PublicSlhdsa, priv.SecretSlhdsa = priv.PublicKey.Slhdsa.DeriveKey(keySeed) + + return priv, nil +} + +// Sign generates a SLH-DSA signature. +func Sign(priv *PrivateKey, message []byte) (signature []byte, err error) { + // The specification of SLH-DSA [FIPS-205] prescribes an optional non-deterministic message randomizer. + // This is not used in this specification + options := slhdsa.SignatureOpts{ + PreHashID: slhdsa.NoPreHash, + IsDeterministic: true, + } + signature, err = priv.SecretSlhdsa.Sign(nil, message, &options) + if err != nil { + return nil, fmt.Errorf("slhdsa: unable to sign with SLH-DSA: %s", err) + } + if signature == nil { + return nil, goerrors.New("slhdsa: unable to sign with SLH-DSA") + } + + return signature, nil +} + +// Verify verifies the SLH-DSA signature. +func Verify(pub *PublicKey, message, dSig []byte) bool { + return pub.Slhdsa.Verify(pub.PublicSlhdsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if !priv.PublicSlhdsa.Equal(priv.SecretSlhdsa.Public()) { + return errors.KeyInvalidError("slhdsa: invalid public key") + } + + return nil +} diff --git a/openpgp/slhdsa/slhdsa_test.go b/openpgp/slhdsa/slhdsa_test.go new file mode 100644 index 000000000..07606699e --- /dev/null +++ b/openpgp/slhdsa/slhdsa_test.go @@ -0,0 +1,87 @@ +// Package slhdsa_test tests the SLH-DSA implementation suitable for OpenPGP, experimental. +package slhdsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" +) + +var algorithms = map[string]packet.PublicKeyAlgorithm{ + "SLH-DSA-SHAKE-128s": packet.PubKeyAlgoSlhdsaShake128s, + "SLH-DSA-SHAKE-128f": packet.PubKeyAlgoSlhdsaShake128f, + "SLH-DSA-SHAKE-256s": packet.PubKeyAlgoSlhdsaShake256s, +} + +func TestValidate(t *testing.T) { + for asymmName, asymmAlgo := range algorithms { + t.Run(asymmName, func(t *testing.T) { + testValidateAlgo(t, asymmAlgo) + }) + } +} + +func TestSignVerify(t *testing.T) { + for asymmName, asymmAlgo := range algorithms { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + }) + } +} + +func testValidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := slhdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicSlhdsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicSlhdsa, err = key.Slhdsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := slhdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *slhdsa.PrivateKey { + scheme, err := packet.GetSlhdsaSchemeFromAlgID(algId) + if err != nil { + t.Errorf("error getting SLH-DSA scheme: %s", err) + } + + priv, err := slhdsa.GenerateKey(rand.Reader, uint8(algId), scheme) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *slhdsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, err := slhdsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := slhdsa.Verify(&priv.PublicKey, digest, dSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 84d328c22..1ac8a841c 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -24,6 +24,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -420,6 +421,17 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) + case packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSH key") + } + + d, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -466,7 +478,8 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) - case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, + packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { return nil, err } diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 08607667b..acc26cd58 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -1158,3 +1159,16 @@ func TestPqcDraftVectors(t *testing.T) { }) } } + +func TestPqcDraftKey(t *testing.T) { + t.Skip("skipping") + secretKey, err := ReadArmoredKeyRing(strings.NewReader(v6SlhDsaMlkem768PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + _, ok := secretKey[0].EncryptionKey(time.Unix(1737373639, 0), nil) + if !ok { + t.Fatal("Failed to verify key") + } +} diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 2e8a68680..2f09991db 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -979,3 +979,364 @@ rD9h0SH7PihV9SRdvR2vvWyn7ygFNPajy/8PTH15eEv/5g6ZWxs5CKvpz0hTqf8C 0lQCCQIMslhjNg7KUOTtedOwUxvAoHK/lZf4fpMbG2GW7r6OHwShQ/zNruQmR8qV qJsN7xv8+utysXtt6SUgMPnF3oUp9HzBnCwHb/m/di69xNsYQAE= -----END PGP MESSAGE-----` + +const v6SlhDsaMlkem768PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +GXjAz110OD7IrJykydmxG+WiVkeP7RdkMCXM5OGNJ/4aXGuTyLCMRjk4gSp6 +V+jYMc28I5EDWfTQagLFeF8d3VTpR38eZxrvj9/jbVc46wXW2ZfCVbj9l2C8 +bybOKpmtNJEFxA+Ya2AcCKtqRyFHfeDD3fNMgtn/PbP7MeOcEVEaBrl7YQQN +V/fx7tskWXF1/xmZVhYmNOrNf854GW3OunSUV8FgMyRGq4dS5W79cGhQBQ23 +sPsZj6Tof88zDbesv8+E4dqPnFlynn4BF9CIB+GJk6v9tSsNsipgGlxSNw4k +wN7Ar0kpRhnbNMk7BGj/IIAbavzMa7rEcHxKeJnCaS6YDjzr8n5WJYC5/7j9 +gvaVNbL61eZ4rEVrE1l4am1uxARHaN+UjrndnzAztyAOHzukTnAnhpRoq2x4 +eHffMeoTIc7xWIGgxg0MENyAas0FR7npxwtU7PaEkLV4NYEK1sTQ5LfccVUV +PXPgkqVVqFMlYPTpJ3zXSe4VewWmqrx7AWjPMNCyW6Va3O3Lq/oUz/5jDfGv +y7EHITH9Oe0avRDyoqfv8Waj4ps1Yb2eGgDDDCnbqlbpXKlBp6WIoIy+2Fs9 +EcYbhCt9vuYknFyD8WN4+xpbc0lPODocRoQqmmhfYedPJ6K5+nmzYRHf2Mn7 +wAp4DFB9O+zc6QFmztc5wab/PB7oIE5BL3fe5nxmne6iIPQhJVD4m/Bo6Qhb ++Guh3LhM9N2dwZnEEpwaT1P27LUuersi53ktc1mac2djj+E13JQCoex/83wa +qowIYHfkFO87dY3wR7ZmYRCUb+72Kq6AGVx3WGDk2IgGmEC4b9DeUxU2ipQl +Skn9+oI18lxjG0gMSgwYeqo80+JDF9dyheztuUUyeP1ArQKDzFnDj/K0WcDg +ELrYi0gp1lTCc5WeBH0/9O35SvJybfqRp/hQHl79vzlbHissGRidyS5doxKu +OyTwypC26m0Sr/O020/v/h3QsITFP5dShRLduE8tV3C/SrtUuvGjqWgQAHbR +kYKlU+YLBa3Dj+LqgeUwW/u2Hj5aiEBUq8zg7TAverEHQ+csbDLyHlLbSDPd +whWaNjAONWzmSUEehjnn6ujQORdtpqLDBqpfTQBbShXZBMMpE4h8G5rbPFLM +sxrNpSx42F6qkVaVULsiEaTbIBYVnrbdK258HKpg2Qydzti1lx4N16unTxIh +T45cEAJK5Jg0OkEihHQEMhzok5WWsKbQs7hyhw6CJv7xpSO7Sh6pvB3Y1Djg +qwBbad3i7yRO9RI2cYnUzjzsstcqj5JPcbL78blZJ4Jv6e9ewMT74JG3bV02 +7Y+XXJm/QpW2mXxdvnC5iu2tQaKPju5JYPa/qyF00/r45994Foi+hcAEyK5O +Tl7ic9aPNHlD57EKuHdMRxvihqJvt+IoHlNJ6EvD71wUpDTyh9nRgSMS7MrM +1gC0MhO4cO2biYLMePGUpmLyK4p9ajKtTqqIZFfL7eJVTHiJEmumW5vJIoYw +GoPOj8mz8as11N2ny50Ys4pv7qhNfkEsGqHI9iiBjojXiiG4Cyi8hVGgnmfT +cWd3pEpUuiQMWBKokikyM/gYk90i01QY7caO95/zWcfKT7uBQime0krKeUaD +LY5p1KmSY/wj7acbs8PYJuW7kFQOKmP33kMEcmJ4VO1rc8F5jarBICCut56L ++H95aBsnt7ObJ5rdZMfgvfnD6sRVV7mLsXqQ4OSYhQsDPqjXgE/rlnGGjiHM +anDBIZPOFBUIlK+mmIvXahyKE6nflbRe4gcNFq5AtFz5E3yMvVKJ42eKck21 +EJwlIDlxOkc9/t8hUngoY3V41sU/Y535FkcBxc3oN9LRnAU8DN7WNIBlAlPB +edOw+lU8HZRlsOuHRQLQ2eboJSR+T890GoaIH/NgPWR9f22E3nm2qcFTd6RV +FMhyLj/uhUlFCdKxlF0aLeyMa40rO/U4yUXcMpnNMTRw7soungOjvy7Frhfb +m9ry4yST5pCNNzsgvCpJmc8hhCfFyNzp6KqBbBamz0hLMcgBQ5kZlOLcbPIM +jbOZ0iVrRojMjhwSD+WNxJNo70cCz96GOnhoXEcMJyxIDKm5s4oR/hqgoDmi +53MgZKWc8qWo8pVy7iqwRRamaXB9DLvovo+PWlSxm91i66JgcoaAyAlnY/U+ +W7aE +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 8509f6fe9..6144153a9 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -1057,11 +1057,11 @@ func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { } } } - case packet.PubKeyAlgoMldsa65Ed25519: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f: return []uint8{ hashToHashId(crypto.SHA3_256), } - case packet.PubKeyAlgoMldsa87Ed448: + case packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoSlhdsaShake256s: return []uint8{ hashToHashId(crypto.SHA3_512), } From 4a0afdc4a0dbc577a6124b5a7a31631fe0af1d34 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 30 Jan 2025 16:08:24 +0100 Subject: [PATCH 13/14] Update codepoints to draft-07 --- openpgp/packet/packet.go | 10 +++++----- openpgp/read_test.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 2d68a82e6..0595a48ac 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -519,11 +519,11 @@ const ( PubKeyAlgoMlkem1024X448 = 106 // Experimental PQC DSA algorithms - PubKeyAlgoMldsa65Ed25519 = 107 - PubKeyAlgoMldsa87Ed448 = 108 - PubKeyAlgoSlhdsaShake128s = 109 - PubKeyAlgoSlhdsaShake128f = 110 - PubKeyAlgoSlhdsaShake256s = 111 + PubKeyAlgoMldsa65Ed25519 = 30 + PubKeyAlgoMldsa87Ed448 = 31 + PubKeyAlgoSlhdsaShake128s = 32 + PubKeyAlgoSlhdsaShake128f = 33 + PubKeyAlgoSlhdsaShake256s = 34 ) // CanEncrypt returns true if it's possible to encrypt a message to a public diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 74040b544..2d2df3767 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -967,12 +967,12 @@ var pqcDraftVectors = map[string]struct { fingerprints []string armoredMessages []string }{ - "v6_Ed25519_ML-KEM-768+X25519": { + /*"v6_Ed25519_ML-KEM-768+X25519": { v6Ed25519Mlkem768X25519PrivateTestVector, v6Ed25519Mlkem768X25519PublicTestVector, []string{"bf262b24177002ac8ae5dc6da47c056d22ab9906d47d07952b75c358021901ca", "48b94bce2f9771788f5feb74122d599989c400cc0f49108bc98e0ea7945e4838"}, []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, - }, + },*/ } func TestPqcDraftVectors(t *testing.T) { From 05536d9ed157a704b04797593ddd886873e5fbe0 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Wed, 29 Jan 2025 16:45:31 +0100 Subject: [PATCH 14/14] (Temp) Generate PQC test vectors --- openpgp/mlkem_ecdh/mlkem_ecdh.go | 6 + openpgp/v2/keys_v6_test.go | 28 ++++ openpgp/v2/pqc_test_vectors_test.go | 247 ++++++++++++++++++++++++++++ pqc-test-vectors-jan-2025.txt | 0 4 files changed, 281 insertions(+) create mode 100644 openpgp/v2/pqc_test_vectors_test.go create mode 100644 pqc-test-vectors-jan-2025.txt diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index ea07c79c3..d509c05ba 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -3,6 +3,7 @@ package mlkem_ecdh import ( + "encoding/hex" goerrors "errors" "fmt" "io" @@ -108,6 +109,8 @@ func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemera if err != nil { return nil, nil, nil, err } + fmt.Printf("sessionKey: %x\n", msg) + fmt.Printf("keyEncryptionKey: %x\n\n", keyEncryptionKey) if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { return nil, nil, nil, err @@ -158,6 +161,9 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK // eccEphemeral - the ECDH ciphertext encoded as an octet string // eccPublicKey - The ECDH public key of the recipient as an octet string + fmt.Printf("ecdh key share: %s\n", hex.EncodeToString(eccKeyShare)) + fmt.Printf("ml-kem key share: %s\n", hex.EncodeToString(mlkemKeyShare)) + // SHA3-256(mlkemKeyShare || eccKeyShare || eccEphemeral || eccPublicKey || // mlkemEphemeral || mlkemPublicKey || algId || "OpenPGPCompositeKDFv1") h := sha3.New256() diff --git a/openpgp/v2/keys_v6_test.go b/openpgp/v2/keys_v6_test.go index 66220a8ad..99540eb66 100644 --- a/openpgp/v2/keys_v6_test.go +++ b/openpgp/v2/keys_v6_test.go @@ -3,10 +3,12 @@ package v2 import ( "bytes" "crypto" + "fmt" "strings" "testing" "time" + "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -242,3 +244,29 @@ func TestReadLegacyECC(t *testing.T) { t.Fatal("should not be able to read v6 legacy ECC key") } } + +func TestGeneratePQCTestVector(t *testing.T) { + c := &packet.Config{ + V6Keys: true, + Algorithm: packet.PubKeyAlgoSlhdsaShake128s, + AEADConfig: &packet.AEADConfig{}, + DefaultCipher: packet.CipherAES256, + DefaultHash: crypto.SHA3_256, + } + e, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", c) + if err != nil { + t.Fatal(err) + } + + var armoredKey bytes.Buffer + armorer, _ := armor.Encode(&armoredKey, "PGP PRIVATE KEY BLOCK", nil) + e.SerializePrivateWithoutSigning(armorer, nil) + armorer.Close() + fmt.Println(armoredKey.String()) + + var armoredKeyPublic bytes.Buffer + armorer, _ = armor.Encode(&armoredKeyPublic, "PGP PUBLIC KEY BLOCK", nil) + e.Serialize(armorer) + armorer.Close() + fmt.Println(armoredKeyPublic.String()) +} diff --git a/openpgp/v2/pqc_test_vectors_test.go b/openpgp/v2/pqc_test_vectors_test.go new file mode 100644 index 000000000..dc10465e3 --- /dev/null +++ b/openpgp/v2/pqc_test_vectors_test.go @@ -0,0 +1,247 @@ +package v2 + +import ( + "bytes" + "crypto" + "fmt" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func dumpTestVector(_ *testing.T, filename, vector string) { + fmt.Printf("Artifact: %s\n%s\n\n", filename, vector) +} + +func serializePqSkVector(t *testing.T, filename string, entity *Entity, doChecksum bool) { + var serializedArmoredPrivate bytes.Buffer + serializedPrivate, err := armor.EncodeWithChecksumOption(&serializedArmoredPrivate, PrivateKeyType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = entity.SerializePrivate(serializedPrivate, nil); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPrivate.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredPrivate.String()) +} + +func serializePqPkVector(t *testing.T, filename string, entity *Entity, doChecksum bool) { + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = entity.Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredPublic.String()) +} + +func encryptPqcMessageVector(t *testing.T, filename string, entity *Entity, config *packet.Config, doChecksum bool) { + var serializedArmoredMessage bytes.Buffer + serializedMessage, err := armor.EncodeWithChecksumOption(&serializedArmoredMessage, MessageType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + w, err := Encrypt(serializedMessage, []*Entity{entity}, nil, []*Entity{entity}, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } + + const message = "Testing\n" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) + } + + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) + } + + err = serializedMessage.Close() + if err != nil { + t.Fatalf("Error closing armoring WriteCloser: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredMessage.String()) +} + +func TestV6EddsaPqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + kyberConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMlkem768X25519, + V6Keys: true, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + err = entity.AddEncryptionSubkey(kyberConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-eddsa-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + configV2 := &packet.Config{ + DefaultCipher: packet.CipherAES256, + } + + encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6MlDsa65PqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-mldsa-65-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-mldsa-65-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-mldsa-65-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6MlDsa87PqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-mldsa-87-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-mldsa-87-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-mldsa-87-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6SlhDsa128sPqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoSlhdsaShake128s, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-slhdsa-128s-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-slhdsa-128s-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-slhdsa-128s-sample-message-v2.asc", entity, configV2, false) +} diff --git a/pqc-test-vectors-jan-2025.txt b/pqc-test-vectors-jan-2025.txt new file mode 100644 index 000000000..e69de29bb