From 29bcd2a0e2c8c77ce30e45848296ba1dc73c7f13 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 12 Jul 2022 16:40:23 +0200 Subject: [PATCH 01/36] Update GitHub workflow branches --- .github/workflows/go.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1a05c919..d0bf0843 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [ main ] + branches: [ main, ProtonMail ] pull_request: - branches: [ main ] + branches: [ main, ProtonMail ] jobs: @@ -44,4 +44,4 @@ jobs: 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 + run: go test -v ./... -run RandomizeSlow -count=32 From bd59a910688c5b090ca3d11a91680e5210fbc181 Mon Sep 17 00:00:00 2001 From: larabr Date: Tue, 12 Jul 2022 16:43:06 +0200 Subject: [PATCH 02/36] Add support for automatic forwarding (#54) --- openpgp/ecdh/ecdh.go | 67 ++++++++++++++++++++++++++------- openpgp/ecdh/ecdh_test.go | 73 +++++++++++++++++++++++++++++++++++- openpgp/forwarding_test.go | 70 ++++++++++++++++++++++++++++++++++ openpgp/packet/public_key.go | 47 ++++++++++++++++++++--- 4 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 openpgp/forwarding_test.go diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index ae3403e9..1e81e747 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -17,8 +17,38 @@ import ( ) type KDF struct { - Hash algorithm.Hash - Cipher algorithm.Cipher + Version int // Defaults to v1; non-standard v2 allows forwarding + Hash algorithm.Hash + Cipher algorithm.Cipher + Flags byte // (v2 only) + ReplacementFingerprint []byte // (v2 only) fingerprint to use instead of recipient's (for v5 keys, the 20 leftmost bytes only) + ReplacementKDFParams []byte // (v2 only) serialized KDF params to use in KDF digest computation +} + +func (kdf *KDF) serialize(w io.Writer) (err error) { + if kdf.Version != 2 { + // Default version is 1 + // Length || Version || Hash || Cipher + if _, err := w.Write([]byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + + return nil + } + + // Length || Version || Hash || Cipher || Flags || (Optional) v2 Fields... + v2Length := byte(4 + len(kdf.ReplacementFingerprint) + len(kdf.ReplacementKDFParams)) + if _, err := w.Write([]byte{v2Length, 2, kdf.Hash.Id(), kdf.Cipher.Id(), kdf.Flags}); err != nil { + return err + } + if _, err := w.Write(kdf.ReplacementFingerprint); err != nil { + return err + } + if _, err := w.Write(kdf.ReplacementKDFParams); err != nil { + return err + } + + return nil } type PublicKey struct { @@ -32,13 +62,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,26 +176,38 @@ 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 pub.KDF.ReplacementKDFParams != nil { + kdf := pub.KDF.ReplacementKDFParams + if _, err := param.Write(kdf); err != nil { + return nil, err + } + } else { + if err := pub.KDF.serialize(param); err != nil { + return nil, err + } + } if _, err := param.Write([]byte("Anonymous Sender ")); err != nil { return nil, err } + + if pub.KDF.ReplacementFingerprint != nil { + fingerprint = pub.KDF.ReplacementFingerprint + } + if _, err := param.Write(fingerprint[:]); err != nil { return nil, err } - if param.Len()-len(curveOID) != 45 { - return nil, errors.New("ecdh: malformed KDF Param") - } // MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param ); h := pub.KDF.Hash.New() diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 1f70b7dd..6f4dffb4 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,74 @@ 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() + + kdfV2Flags0x01 := KDF{ + Version: 2, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + Flags: 0x01, + ReplacementFingerprint: testFingerprint, + } + expectBytesV2Flags0x01 := []byte{24, 2, kdfV2Flags0x01.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x01} + expectBytesV2Flags0x01 = append(expectBytesV2Flags0x01, testFingerprint...) + + kdfV2Flags0x01.serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2Flags0x01) { + t.Errorf("error serializing KDF params v2 (flags 0x01), got %x, want: %x", gotBytes, expectBytesV2Flags0x01) + } + byteBuffer.Reset() + + kdfV2Flags0x02 := KDF{ + Version: 2, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + Flags: 0x02, + ReplacementKDFParams: expectBytesV1, + } + expectBytesV2Flags0x02 := []byte{8, 2, kdfV2Flags0x02.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x02} + expectBytesV2Flags0x02 = append(expectBytesV2Flags0x02, expectBytesV1...) + + kdfV2Flags0x02.serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2Flags0x02) { + t.Errorf("error serializing KDF params v2 (flags 0x02), got %x, want: %x", gotBytes, expectBytesV2Flags0x02) + } + byteBuffer.Reset() + + kdfV2Flags0x03 := KDF{ + Version: 2, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + Flags: 0x03, + ReplacementFingerprint: testFingerprint, + ReplacementKDFParams: expectBytesV1, + } + expectBytesV2Flags0x03 := []byte{28, 2, kdfV2Flags0x03.Hash.Id(), kdfV2Flags0x03.Cipher.Id(), 0x03} + expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, testFingerprint...) + expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, expectBytesV1...) + + kdfV2Flags0x03.serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2Flags0x03) { + t.Errorf("error serializing KDF params v2 (flags 0x03), got %x, want: %x", gotBytes, expectBytesV2Flags0x03) + } + byteBuffer.Reset() +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go new file mode 100644 index 00000000..9b286a8e --- /dev/null +++ b/openpgp/forwarding_test.go @@ -0,0 +1,70 @@ +package openpgp + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" + + "golang.org/x/crypto/openpgp/armor" +) + +var ( + charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: OpenPGP.js v4.10.4 +Comment: https://openpgpjs.org + +xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI +MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt +zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4 +BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz ++W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY +3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX +VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe +7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p +Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP +WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS +3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID +=FVvG +-----END PGP PRIVATE KEY BLOCK-----` + + fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- +Version: OpenPGP.js v4.10.4 +Comment: https://openpgpjs.org + +wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M +Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR +Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU +T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI= +=3iIr +-----END PGP MESSAGE-----` +) + +func TestForwardingDecryption(t *testing.T) { + charlieKey, err := ReadArmoredKeyRing(bytes.NewBufferString(charlieKeyArmored)) + if err != nil { + t.Error(err) + return + } + ciphertext, err := armor.Decode(strings.NewReader(string(fwdCiphertextArmored))) + if err != nil { + t.Error(err) + return + } + // Decrypt message + md, err := ReadMessage(ciphertext.Body, charlieKey, nil, nil) + if err != nil { + t.Error(err) + return + } + body, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatal(err) + } + + expectedBody := "Hello Bob, hello world" + gotBody := string(body) + if gotBody != expectedBody { + t.Fatal("Decrypted body did not match expected body") + } +} diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f279789d..2f58d6ee 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -451,11 +451,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 != 1 && kdfVersion != 2 { + return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(int(kdfVersion))) } kdfHash, ok := algorithm.HashById[pk.kdf.Bytes()[1]] if !ok { @@ -466,10 +468,45 @@ 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 == 2 { + if kdfLen < 4 { + return errors.UnsupportedError("unsupported ECDH KDF v2 length: " + strconv.Itoa(kdfLen)) + } + + kdf.Flags = pk.kdf.Bytes()[3] + readBytes := 4 + if kdf.Flags&0x01 != 0x0 { + // Expect 20-byte fingerprint + if kdfLen < readBytes+20 { + return errors.UnsupportedError("malformed ECDH KDF params") + } + kdf.ReplacementFingerprint = pk.kdf.Bytes()[readBytes : readBytes+20] + readBytes += 20 + } + + if kdf.Flags&0x02 != 0x0 { + // Expect replacement params + // Read length field + if kdfLen < readBytes+1 { + return errors.UnsupportedError("malformed ECDH KDF params") + } + fieldLen := int(pk.kdf.Bytes()[readBytes]) + 1 // Account for length field + if kdfLen < readBytes+fieldLen { + return errors.UnsupportedError("malformed ECDH KDF params") + } + kdf.ReplacementKDFParams = pk.kdf.Bytes()[readBytes : readBytes+fieldLen] + } + } + + ecdhKey := ecdh.NewPublicKey(c, kdf) err = ecdhKey.UnmarshalPoint(pk.p.Bytes()) pk.PublicKey = ecdhKey - return } From 783ef59770c0fbe3ad83800e0c8dcf3ccf848765 Mon Sep 17 00:00:00 2001 From: Kostis Andrikopoulos Date: Tue, 12 Jul 2022 18:02:19 +0200 Subject: [PATCH 03/36] 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 | 6 + openpgp/keys.go | 12 ++ openpgp/keys_test.go | 187 ++++++++++++++++++ openpgp/packet/encrypted_key.go | 56 +++++- openpgp/packet/encrypted_key_test.go | 31 +++ openpgp/packet/packet.go | 7 +- openpgp/packet/private_key.go | 94 +++++++++ openpgp/packet/public_key.go | 105 ++++++++++ openpgp/packet/signature.go | 19 +- openpgp/packet/signature_test.go | 27 +++ openpgp/read.go | 2 +- openpgp/read_test.go | 7 + openpgp/read_write_test_data.go | 18 ++ openpgp/symmetric/aead.go | 76 +++++++ openpgp/symmetric/hmac.go | 95 +++++++++ openpgp/write_test.go | 85 ++++++++ 18 files changed, 932 insertions(+), 6 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 00000000..0c3b9123 --- /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 00000000..6544b4ec --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "testing" + "bytes" +) + +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 a40e45be..099e1dfd 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" ) @@ -308,6 +309,8 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { return nil, err } return priv, nil + case packet.ExperimentalPubKeyAlgoHMAC: + return symmetric.HMACGenerateKey(config.Random(), config.Hash()) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -350,6 +353,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 a071353e..284a941c 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 3cb4ac00..2eea7d67 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -19,6 +19,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" @@ -1169,6 +1170,192 @@ 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.Compare(parsedPrivateKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("parsed wrong key") + } + if bytes.Compare(parsedPublicKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("parsed wrong key in public part") + } + if bytes.Compare(generatedPublicKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("generated Public and Private Key differ") + } + + if bytes.Compare(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) != 0 { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if bytes.Compare(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) != 0 { + 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.Compare(parsedPrivateKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("parsed wrong key") + } + if bytes.Compare(parsedPublicKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("parsed wrong key in public part") + } + if bytes.Compare(generatedPublicKey.Key, generatedPrivateKey.Key) != 0 { + t.Error("generated Public and Private Key differ") + } + + if bytes.Compare(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) != 0 { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if bytes.Compare(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) != 0 { + 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, diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 58340945..bd4de6bf 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))) } @@ -415,7 +438,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, 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))) } @@ -554,6 +579,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 [10]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 := 10 /* 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 787c7fec..5ed0a8ed 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 1e92e22c..dd4ad34c 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 f04e6c6b..9dde78ec 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -31,6 +31,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" "golang.org/x/crypto/hkdf" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) // PrivateKey represents a possibly encrypted private key. See RFC 4880, @@ -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") } @@ -183,10 +186,15 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey) case *ecdh.PrivateKey: pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey) +<<<<<<< HEAD case *x25519.PrivateKey: pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) case *x448.PrivateKey: pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) +======= + case *symmetric.AEADPrivateKey: + pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) +>>>>>>> 3731c9c (openpgp: Add support for symmetric subkeys (#74)) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -530,6 +538,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 +856,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 +891,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 +1155,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 2f58d6ee..eb663bd1 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,6 +5,7 @@ package packet import ( + "crypto" "crypto/dsa" "crypto/rsa" "crypto/sha1" @@ -28,6 +29,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" ) @@ -229,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 @@ -279,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))) } @@ -603,6 +633,53 @@ 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 + } + + symmetric := &symmetric.HMACPublicKey{ + Hash: crypto.Hash(hash[0]), + 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 { @@ -690,6 +767,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") } @@ -782,6 +862,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{uint8(symmKey.Hash)} + if _, err = w.Write(hashOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -868,6 +964,13 @@ 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) + + if !HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) { + return errors.SignatureError("HMAC verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1089,6 +1192,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 90097951..c2557371 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -65,6 +65,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 @@ -172,7 +173,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 @@ -310,6 +311,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") } @@ -953,6 +959,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))) } @@ -1058,7 +1069,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") } @@ -1079,6 +1090,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") } @@ -1185,6 +1198,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 5443243f..6c7a6c63 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 8a69b44a..43def2c4 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 318d927e..78baa19c 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -28,6 +28,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 { diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 670d6022..77282c0e 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 00000000..044b1394 --- /dev/null +++ b/openpgp/symmetric/aead.go @@ -0,0 +1,76 @@ +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 00000000..c7e15d2d --- /dev/null +++ b/openpgp/symmetric/hmac.go @@ -0,0 +1,95 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "crypto/sha256" + "io" +) + +type HMACPublicKey struct { + Hash crypto.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 crypto.Hash) (priv *HMACPrivateKey, err error) { + priv, err = generatePrivatePartHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartHMAC(hash) + return +} + +func generatePrivatePartHMAC(rand io.Reader, hash crypto.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 crypto.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 := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *HMACPublicKey) Verify(digest []byte, signature []byte) bool { + expectedMAC := calculateMAC(pub.Hash, pub.Key, digest) + return hmac.Equal(expectedMAC, signature) +} + +func calculateMAC(hash crypto.Hash, key []byte, data []byte) []byte { + mac := hmac.New(hash.New, key) + mac.Write(data) + + return mac.Sum(nil) +} diff --git a/openpgp/write_test.go b/openpgp/write_test.go index c928236b..3cd03d85 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,90 @@ 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) + } + + var list []*Entity + 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.Compare(dec, []byte(message)) != 0 { + 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) + } + var list []*Entity + 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 37452a31e8c0b8b98c106cbf459a71474d4254ff Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 13 Jul 2022 20:37:15 +0200 Subject: [PATCH 04/36] Fix HMAC hash algorithm ID parsing and serializing --- openpgp/key_generation.go | 3 ++- openpgp/keys_test.go | 34 ++++++++++++++++++++++++++++++++++ openpgp/packet/public_key.go | 16 ++++++++++++---- openpgp/symmetric/hmac.go | 36 +++++++++++++++++++++++++----------- 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 099e1dfd..bcf23175 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -310,7 +310,8 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return priv, nil case packet.ExperimentalPubKeyAlgoHMAC: - return symmetric.HMACGenerateKey(config.Random(), config.Hash()) + hash := algorithm.HashById[hashToHashId(config.Hash())] + return symmetric.HMACGenerateKey(config.Random(), hash) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 2eea7d67..26b14571 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2052,3 +2052,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/public_key.go b/openpgp/packet/public_key.go index eb663bd1..8f43d3e3 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,7 +5,6 @@ package packet import ( - "crypto" "crypto/dsa" "crypto/rsa" "crypto/sha1" @@ -666,8 +665,13 @@ func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { return } + hmacHash, ok := algorithm.HashById[hash[0]] + if !ok { + return errors.UnsupportedError("unsupported HMAC hash: " + strconv.Itoa(int(hash[0]))) + } + symmetric := &symmetric.HMACPublicKey{ - Hash: crypto.Hash(hash[0]), + Hash: hmacHash, BindingHash: bindingHash, } @@ -872,7 +876,7 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { return case ExperimentalPubKeyAlgoHMAC: symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) - hashOctet := [1]byte{uint8(symmKey.Hash)} + hashOctet := [1]byte{symmKey.Hash.Id()} if _, err = w.Write(hashOctet[:]); err != nil { return } @@ -967,7 +971,11 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro case ExperimentalPubKeyAlgoHMAC: HMACKey := pk.PublicKey.(*symmetric.HMACPublicKey) - if !HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) { + result, err := HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) + if err != nil { + return err + } + if !result { return errors.SignatureError("HMAC verification failure") } return nil diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go index c7e15d2d..fd4a7cbb 100644 --- a/openpgp/symmetric/hmac.go +++ b/openpgp/symmetric/hmac.go @@ -5,10 +5,13 @@ import ( "crypto/hmac" "crypto/sha256" "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" ) type HMACPublicKey struct { - Hash crypto.Hash + 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 @@ -24,7 +27,7 @@ type HMACPrivateKey struct { Key []byte } -func HMACGenerateKey(rand io.Reader, hash crypto.Hash) (priv *HMACPrivateKey, err error) { +func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { priv, err = generatePrivatePartHMAC(rand, hash) if err != nil { return @@ -34,7 +37,7 @@ func HMACGenerateKey(rand io.Reader, hash crypto.Hash) (priv *HMACPrivateKey, er return } -func generatePrivatePartHMAC(rand io.Reader, hash crypto.Hash) (priv *HMACPrivateKey, err error) { +func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { priv = new(HMACPrivateKey) var seed [32] byte _, err = rand.Read(seed[:]) @@ -53,7 +56,7 @@ func generatePrivatePartHMAC(rand io.Reader, hash crypto.Hash) (priv *HMACPrivat return } -func (priv *HMACPrivateKey) generatePublicPartHMAC(hash crypto.Hash) (err error) { +func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { priv.PublicKey.Hash = hash bindingHash := ComputeBindingHash(priv.HashSeed) @@ -76,20 +79,31 @@ func (priv *HMACPrivateKey) Public() crypto.PublicKey { } func (priv *HMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { - expectedMAC := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + 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 { - expectedMAC := calculateMAC(pub.Hash, pub.Key, digest) - return hmac.Equal(expectedMAC, signature) +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 crypto.Hash, key []byte, data []byte) []byte { - mac := hmac.New(hash.New, key) +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) + return mac.Sum(nil), nil } From febcea6d5d1dcc43eb1053f769ced1b1083b0c2a Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 17 Jan 2023 10:12:07 +0100 Subject: [PATCH 05/36] Rename branch to Proton --- .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 d0bf0843..7ef309d6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [ main, ProtonMail ] + branches: [ main ] pull_request: - branches: [ main, ProtonMail ] + branches: [ main, Proton ] jobs: From c2b7cfe36dd999b042a195c4c3964498d7022833 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Fri, 24 Feb 2023 10:43:10 +0100 Subject: [PATCH 06/36] Add full forwarding support --- openpgp/ecdh/ecdh.go | 97 ++- openpgp/ecdh/ecdh_test.go | 55 +- openpgp/errors/errors.go | 2 + openpgp/forwarding.go | 77 +++ openpgp/forwarding_test.go | 176 ++++-- openpgp/internal/ecc/curve25519.go | 3 +- openpgp/internal/ecc/curve25519/curve25519.go | 124 ++++ .../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 | 13 + .../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 | 23 + openpgp/packet/public_key.go | 61 +- openpgp/packet/signature.go | 13 +- 23 files changed, 2454 insertions(+), 151 deletions(-) create mode 100644 openpgp/forwarding.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 diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index 1e81e747..85a06b17 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -12,42 +12,49 @@ 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 { - Version int // Defaults to v1; non-standard v2 allows forwarding + Version int // Defaults to v1; 255 for forwarding Hash algorithm.Hash Cipher algorithm.Cipher - Flags byte // (v2 only) - ReplacementFingerprint []byte // (v2 only) fingerprint to use instead of recipient's (for v5 keys, the 20 leftmost bytes only) - ReplacementKDFParams []byte // (v2 only) serialized KDF params to use in KDF digest computation + ReplacementFingerprint []byte // (forwarding only) fingerprint to use instead of recipient's (20 octets) } -func (kdf *KDF) serialize(w io.Writer) (err error) { - if kdf.Version != 2 { - // Default version is 1 - // Length || Version || Hash || Cipher - if _, err := w.Write([]byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { +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") } +} - // Length || Version || Hash || Cipher || Flags || (Optional) v2 Fields... - v2Length := byte(4 + len(kdf.ReplacementFingerprint) + len(kdf.ReplacementKDFParams)) - if _, err := w.Write([]byte{v2Length, 2, kdf.Hash.Id(), kdf.Cipher.Id(), kdf.Flags}); err != nil { - return err - } - if _, err := w.Write(kdf.ReplacementFingerprint); err != nil { - return err - } - if _, err := w.Write(kdf.ReplacementKDFParams); err != nil { +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 } @@ -187,16 +194,11 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead if _, err := param.Write(algo); err != nil { return nil, err } - if pub.KDF.ReplacementKDFParams != nil { - kdf := pub.KDF.ReplacementKDFParams - if _, err := param.Write(kdf); err != nil { - return nil, err - } - } else { - if err := pub.KDF.serialize(param); 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 } @@ -205,7 +207,7 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead fingerprint = pub.KDF.ReplacementFingerprint } - if _, err := param.Write(fingerprint[:]); err != nil { + if _, err := param.Write(fingerprint); err != nil { return nil, err } @@ -246,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 6f4dffb4..0e79778f 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -42,7 +42,7 @@ func TestCurves(t *testing.T) { } func testGenerate(t *testing.T, curve ecc.ECDHCurve) *PrivateKey { - kdf := KDF{ + kdf := KDF { Hash: algorithm.SHA512, Cipher: algorithm.AES256, } @@ -123,63 +123,26 @@ func TestKDFParamsWrite(t *testing.T) { testFingerprint := make([]byte, 20) expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()} - kdf.serialize(byteBuffer) + 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() - kdfV2Flags0x01 := KDF{ - Version: 2, - Hash: algorithm.SHA512, - Cipher: algorithm.AES256, - Flags: 0x01, - ReplacementFingerprint: testFingerprint, - } - expectBytesV2Flags0x01 := []byte{24, 2, kdfV2Flags0x01.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x01} - expectBytesV2Flags0x01 = append(expectBytesV2Flags0x01, testFingerprint...) - - kdfV2Flags0x01.serialize(byteBuffer) - gotBytes = byteBuffer.Bytes() - if !bytes.Equal(gotBytes, expectBytesV2Flags0x01) { - t.Errorf("error serializing KDF params v2 (flags 0x01), got %x, want: %x", gotBytes, expectBytesV2Flags0x01) - } - byteBuffer.Reset() - - kdfV2Flags0x02 := KDF{ - Version: 2, - Hash: algorithm.SHA512, - Cipher: algorithm.AES256, - Flags: 0x02, - ReplacementKDFParams: expectBytesV1, - } - expectBytesV2Flags0x02 := []byte{8, 2, kdfV2Flags0x02.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x02} - expectBytesV2Flags0x02 = append(expectBytesV2Flags0x02, expectBytesV1...) - - kdfV2Flags0x02.serialize(byteBuffer) - gotBytes = byteBuffer.Bytes() - if !bytes.Equal(gotBytes, expectBytesV2Flags0x02) { - t.Errorf("error serializing KDF params v2 (flags 0x02), got %x, want: %x", gotBytes, expectBytesV2Flags0x02) - } - byteBuffer.Reset() - - kdfV2Flags0x03 := KDF{ - Version: 2, + kdfV2 := KDF{ + Version: KDFVersionForwarding, Hash: algorithm.SHA512, Cipher: algorithm.AES256, - Flags: 0x03, ReplacementFingerprint: testFingerprint, - ReplacementKDFParams: expectBytesV1, } - expectBytesV2Flags0x03 := []byte{28, 2, kdfV2Flags0x03.Hash.Id(), kdfV2Flags0x03.Cipher.Id(), 0x03} - expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, testFingerprint...) - expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, expectBytesV1...) + expectBytesV2 := []byte{23, 0xFF, kdfV2.Hash.Id(), kdfV2.Cipher.Id()} + expectBytesV2 = append(expectBytesV2, testFingerprint...) - kdfV2Flags0x03.serialize(byteBuffer) + kdfV2.Serialize(byteBuffer) gotBytes = byteBuffer.Bytes() - if !bytes.Equal(gotBytes, expectBytesV2Flags0x03) { - t.Errorf("error serializing KDF params v2 (flags 0x03), got %x, want: %x", gotBytes, expectBytesV2Flags0x03) + 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 c42b01cb..c20292c6 100644 --- a/openpgp/errors/errors.go +++ b/openpgp/errors/errors.go @@ -33,6 +33,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 00000000..14a79a66 --- /dev/null +++ b/openpgp/forwarding.go @@ -0,0 +1,77 @@ +// 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" +) + +func (e *Entity) NewForwardingEntity(config *packet.Config) (forwardeeKey *Entity, proxyParam []byte, err error) { + encryptionSubKey, ok := e.EncryptionKey(config.Now()) + if !ok { + return nil, nil, errors.InvalidArgumentError("no valid encryption key found") + } + + if encryptionSubKey.PublicKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported encryption subkey version") + } + + if encryptionSubKey.PrivateKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } + + ecdhKey, ok := encryptionSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not type ECDH") + } + + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + id := e.PrimaryIdentity().UserId + + forwardeeKey, err = NewEntity(id.Name, id.Comment, id.Email, config) + if err != nil { + return nil, nil, err + } + + forwardeeEcdhKey, ok := forwardeeKey.Subkeys[0].PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + proxyParam, err = ecdh.DeriveProxyParam(ecdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: ecdhKey.KDF.Hash, + Cipher: ecdhKey.KDF.Cipher, + ReplacementFingerprint: encryptionSubKey.PublicKey.Fingerprint, + } + + err = forwardeeKey.Subkeys[0].PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // 0x04 - This key may be used to encrypt communications. + forwardeeKey.Subkeys[0].Sig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeKey.Subkeys[0].Sig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeKey.Subkeys[0].Sig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeKey.Subkeys[0].Sig.FlagForward = true + + return forwardeeKey, proxyParam, nil +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index 9b286a8e..5d25207f 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -2,69 +2,165 @@ package openpgp import ( "bytes" + "crypto/rand" + goerrors "errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "golang.org/x/crypto/openpgp/armor" + "io" "io/ioutil" "strings" "testing" - - "golang.org/x/crypto/openpgp/armor" ) -var ( - charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org - -xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI -MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt -zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4 -BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz -+W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY -3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX -VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe -7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p -Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP -WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS -3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID -=FVvG +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEY/ikABYJKwYBBAHaRw8BAQdAzz/nPfhJnoAYwg43AFYzxX1v6UwGmfN9jPiI +/MOFxFgAAQDTqvO94jZPb9brhpwayNI9QlqqTlvDP6AH8CpXUfoVmxDczRNib2Ig +PGJvYkBwcm90b24ubWU+wooEExYIADwFAmP4pAAJkIdp9lyYAlNMFiEEzW5s1IvY +GXCwcJkZh2n2XJgCU0wCGwMCHgECGQECCwcCFQgCFgACIgEAAPmGAQDxysrSwxQO +27X/eg7xSE5JVXT7bt8cEZOE+iC2IDS02QEA2CvXnZJK4AOmPsFWKzn3HkFxCybc +CefzoJe0Pp4QNwPHcQRj+KQAEgorBgEEAZdVAQUBAQdArC6ijiQbE4ddGzqYHuq3 +0rV05YYDP+5GtCecalGVizUX/woJzG7AoQ/hzzDi4rf+is90WDIIeHwAAP9JzVrf +QzMRicxCz1PbXNRW/OwKHg0X0bH3MA5A/j3mcBCrwngEGBYIACoFAmP4pAAJkIdp +9lyYAlNMFiEEzW5s1IvYGXCwcJkZh2n2XJgCU0wCG1AAAN0hAP9kJ/CQDBAwrVj5 +92/mkV/4bEWAql/jEEfbBTAGHEb+5wD/ca5jm4FThIaGNO/mLtbkodfR0RTQ5usZ +Xvoo9PdnBQg= +=7A/f -----END PGP PRIVATE KEY BLOCK-----` - fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org +const forwardedMessage = `-----BEGIN PGP MESSAGE----- -wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M -Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR -Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU -T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI= -=3iIr +wV4Dwkk3ytpHrqASAQdAzPWbm24Uj6OYSDaauOuFMRPPLr5zWKXgvC1eHPD78ykw +YkvxNCwD6hfzjLoASVv9jhHJoXY+Pag6QHvoFuMn+hdG90yFh5HMFyileY/CTrT7 +0kcBAPalcAq/OH/pBtIhGT/TKS88IIkz2aSukjbQRf+JNyh7bF+uXVDGmD8zOGa8 +mM9TmGOf8Vi3sjgVAQ5rZQzh36HrBDloBA== +=PotS -----END PGP MESSAGE-----` -) -func TestForwardingDecryption(t *testing.T) { - charlieKey, err := ReadArmoredKeyRing(bytes.NewBufferString(charlieKeyArmored)) +const forwardedPlaintext = "Hello Bob, hello world" + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) if err != nil { t.Error(err) return } - ciphertext, err := armor.Decode(strings.NewReader(string(fwdCiphertextArmored))) + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) if err != nil { t.Error(err) return } - // Decrypt message - md, err := ReadMessage(ciphertext.Body, charlieKey, nil, nil) + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) if err != nil { - t.Error(err) - return + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if bytes.Compare(dec, []byte(forwardedPlaintext)) != 0 { + t.Fatal("forwarded decrypted does not match original") } - body, err := ioutil.ReadAll(md.UnverifiedBody) +} + +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, proxyParam, err := bobEntity.NewForwardingEntity(keyConfig) + if err != nil { + t.Fatal(err) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil) if err != nil { t.Fatal(err) } - expectedBody := "Hello Bob, hello world" - gotBody := string(body) - if gotBody != expectedBody { - t.Fatal("Decrypted body did not match expected body") + _, 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.Compare(dec, plaintext) != 0 { + t.Fatal("decrypted does not match original") + } + + // Forward message + 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: + err = p.ProxyTransform( + proxyParam, + charlesEntity.Subkeys[0].PublicKey.KeyId, + bobEntity.Subkeys[0].PublicKey.KeyId, + ) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = p.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + // 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.Compare(dec, plaintext) != 0 { + t.Fatal("forwarded decrypted does not match original") } } diff --git a/openpgp/internal/ecc/curve25519.go b/openpgp/internal/ecc/curve25519.go index 888767c4..a6721ff9 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 00000000..d5a55088 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519.go @@ -0,0 +1,124 @@ +// 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, + ) + + proxyParam = proxyTransform.Bytes() + + // convert to small endian + reverse(proxyParam) + + 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()) +} + +func reverse(in []byte) { + for i, j := 0, len(in)-1; i < j; i, j = i+1, j-1 { + in[i], in[j] = in[j], in[i] + } +} \ No newline at end of file diff --git a/openpgp/internal/ecc/curve25519/curve25519_test.go b/openpgp/internal/ecc/curve25519/curve25519_test.go new file mode 100644 index 00000000..88921267 --- /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 00000000..ca841ad9 --- /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 00000000..5ad81df0 --- /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 00000000..44dc8e8c --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.go @@ -0,0 +1,13 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +// +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 00000000..293f013c --- /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 00000000..ddb6c9b8 --- /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 00000000..af459ef5 --- /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 00000000..5c91e458 --- /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 00000000..234a5b2e --- /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 00000000..77dc06cf --- /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 00000000..7b5b78cb --- /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 00000000..b484459f --- /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 284a941c..bbcc95d9 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -371,7 +371,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}) } } @@ -762,7 +762,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) @@ -794,14 +794,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 bd4de6bf..7dada811 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -463,6 +463,29 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } +func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwardeeKeyId, forwardingKeyId uint64) error { + if e.Algo != PubKeyAlgoECDH { + return errors.InvalidArgumentError("invalid PKESK") + } + + if e.KeyId != 0 && e.KeyId != forwardingKeyId { + return errors.InvalidArgumentError("invalid key id in PKESK") + } + + ephemeral := e.encryptedMPI1.Bytes() + transformed, err := ecdh.ProxyTransform(ephemeral, proxyParam) + if err != nil { + return err + } + + e.encryptedMPI1 = encoding.NewMPI(transformed) + if e.KeyId != 0 { + e.KeyId = forwardeeKeyId + } + + return 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/public_key.go b/openpgp/packet/public_key.go index 8f43d3e3..5b16eba9 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,12 +5,14 @@ package packet import ( + "bytes" "crypto/dsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" _ "crypto/sha512" "encoding/binary" + goerrors "errors" "fmt" "hash" "io" @@ -69,6 +71,26 @@ func (pk *PublicKey) UpgradeToV6() { pk.setFingerprintAndKeyId() } +// 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 { @@ -485,8 +507,8 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError("unsupported ECDH KDF length: " + strconv.Itoa(kdfLen)) } kdfVersion := int(pk.kdf.Bytes()[0]) - if kdfVersion != 1 && kdfVersion != 2 { - return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(int(kdfVersion))) + 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 { @@ -503,34 +525,12 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { Cipher: kdfCipher, } - if kdfVersion == 2 { - if kdfLen < 4 { + if kdfVersion == ecdh.KDFVersionForwarding { + if pk.Version != 4 || kdfLen != 23 { return errors.UnsupportedError("unsupported ECDH KDF v2 length: " + strconv.Itoa(kdfLen)) } - kdf.Flags = pk.kdf.Bytes()[3] - readBytes := 4 - if kdf.Flags&0x01 != 0x0 { - // Expect 20-byte fingerprint - if kdfLen < readBytes+20 { - return errors.UnsupportedError("malformed ECDH KDF params") - } - kdf.ReplacementFingerprint = pk.kdf.Bytes()[readBytes : readBytes+20] - readBytes += 20 - } - - if kdf.Flags&0x02 != 0x0 { - // Expect replacement params - // Read length field - if kdfLen < readBytes+1 { - return errors.UnsupportedError("malformed ECDH KDF params") - } - fieldLen := int(pk.kdf.Bytes()[readBytes]) + 1 // Account for length field - if kdfLen < readBytes+fieldLen { - return errors.UnsupportedError("malformed ECDH KDF params") - } - kdf.ReplacementKDFParams = pk.kdf.Bytes()[readBytes : readBytes+fieldLen] - } + kdf.ReplacementFingerprint = pk.kdf.Bytes()[3:23] } ecdhKey := ecdh.NewPublicKey(c, kdf) @@ -1049,6 +1049,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 c2557371..c32d7ad2 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -31,7 +31,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -102,8 +102,9 @@ type Signature struct { // FlagsValid is set if any flags were given. See RFC 4880, section // 5.2.3.21 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 4880, section 5.2.3.23 for details. @@ -548,6 +549,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 } @@ -1299,6 +1303,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp if sig.FlagAuthenticate { flags |= KeyFlagAuthenticate } + if sig.FlagForward { + flags |= KeyFlagForward + } if sig.FlagGroupKey { flags |= KeyFlagGroupKey } From 197f38dd352fc653e7b731ba7b62acc87a45b155 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 7 Mar 2023 18:08:39 +0100 Subject: [PATCH 07/36] Use forwardee idenitity in forwarding key generation --- openpgp/forwarding.go | 5 +-- openpgp/forwarding_test.go | 38 +++++++++---------- openpgp/internal/ecc/curve25519/curve25519.go | 16 ++++---- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index 14a79a66..0e76e56b 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -11,7 +11,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/packet" ) -func (e *Entity) NewForwardingEntity(config *packet.Config) (forwardeeKey *Entity, proxyParam []byte, err error) { +func (e *Entity) NewForwardingEntity(name, comment, email string, config *packet.Config) (forwardeeKey *Entity, proxyParam []byte, err error) { encryptionSubKey, ok := e.EncryptionKey(config.Now()) if !ok { return nil, nil, errors.InvalidArgumentError("no valid encryption key found") @@ -32,9 +32,8 @@ func (e *Entity) NewForwardingEntity(config *packet.Config) (forwardeeKey *Entit config.Algorithm = packet.PubKeyAlgoEdDSA config.Curve = packet.Curve25519 - id := e.PrimaryIdentity().UserId - forwardeeKey, err = NewEntity(id.Name, id.Comment, id.Email, config) + forwardeeKey, err = NewEntity(name, comment, email, config) if err != nil { return nil, nil, err } diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index 5d25207f..1307b1c7 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -14,30 +14,30 @@ import ( const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- -xVgEY/ikABYJKwYBBAHaRw8BAQdAzz/nPfhJnoAYwg43AFYzxX1v6UwGmfN9jPiI -/MOFxFgAAQDTqvO94jZPb9brhpwayNI9QlqqTlvDP6AH8CpXUfoVmxDczRNib2Ig -PGJvYkBwcm90b24ubWU+wooEExYIADwFAmP4pAAJkIdp9lyYAlNMFiEEzW5s1IvY -GXCwcJkZh2n2XJgCU0wCGwMCHgECGQECCwcCFQgCFgACIgEAAPmGAQDxysrSwxQO -27X/eg7xSE5JVXT7bt8cEZOE+iC2IDS02QEA2CvXnZJK4AOmPsFWKzn3HkFxCybc -CefzoJe0Pp4QNwPHcQRj+KQAEgorBgEEAZdVAQUBAQdArC6ijiQbE4ddGzqYHuq3 -0rV05YYDP+5GtCecalGVizUX/woJzG7AoQ/hzzDi4rf+is90WDIIeHwAAP9JzVrf -QzMRicxCz1PbXNRW/OwKHg0X0bH3MA5A/j3mcBCrwngEGBYIACoFAmP4pAAJkIdp -9lyYAlNMFiEEzW5s1IvYGXCwcJkZh2n2XJgCU0wCG1AAAN0hAP9kJ/CQDBAwrVj5 -92/mkV/4bEWAql/jEEfbBTAGHEb+5wD/ca5jm4FThIaGNO/mLtbkodfR0RTQ5usZ -Xvoo9PdnBQg= -=7A/f +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----- -wV4Dwkk3ytpHrqASAQdAzPWbm24Uj6OYSDaauOuFMRPPLr5zWKXgvC1eHPD78ykw -YkvxNCwD6hfzjLoASVv9jhHJoXY+Pag6QHvoFuMn+hdG90yFh5HMFyileY/CTrT7 -0kcBAPalcAq/OH/pBtIhGT/TKS88IIkz2aSukjbQRf+JNyh7bF+uXVDGmD8zOGa8 -mM9TmGOf8Vi3sjgVAQ5rZQzh36HrBDloBA== -=PotS +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa -----END PGP MESSAGE-----` -const forwardedPlaintext = "Hello Bob, hello world" +const forwardedPlaintext = "Message for Bob" func TestForwardingStatic(t *testing.T) { charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) @@ -78,7 +78,7 @@ func TestForwardingFull(t *testing.T) { t.Fatal(err) } - charlesEntity, proxyParam, err := bobEntity.NewForwardingEntity(keyConfig) + charlesEntity, proxyParam, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig) if err != nil { t.Fatal(err) } diff --git a/openpgp/internal/ecc/curve25519/curve25519.go b/openpgp/internal/ecc/curve25519/curve25519.go index d5a55088..21670a82 100644 --- a/openpgp/internal/ecc/curve25519/curve25519.go +++ b/openpgp/internal/ecc/curve25519/curve25519.go @@ -29,10 +29,14 @@ func DeriveProxyParam(recipientSecretByte, forwardeeSecretByte []byte) (proxyPar curveGroup, ) - proxyParam = proxyTransform.Bytes() + rawProxyParam := proxyTransform.Bytes() - // convert to small endian - reverse(proxyParam) + // 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 } @@ -116,9 +120,3 @@ func scalarMult(dst, scalar, point *[32]byte) { x2.Multiply(&x2, &z2) copy(dst[:], x2.Bytes()) } - -func reverse(in []byte) { - for i, j := 0, len(in)-1; i < j; i, j = i+1, j-1 { - in[i], in[j] = in[j], in[i] - } -} \ No newline at end of file From c7c4f3633cd0fb8845db484202c2c3d1287c1085 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Mon, 20 Mar 2023 13:43:19 +0100 Subject: [PATCH 08/36] Convert all valid subkeys when issuing a forwarding key --- openpgp/forwarding.go | 168 ++++++++++++++++++++++++-------- openpgp/forwarding_test.go | 69 ++++++++++--- openpgp/packet/encrypted_key.go | 4 +- 3 files changed, 184 insertions(+), 57 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index 0e76e56b..3e447782 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -11,66 +11,154 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/packet" ) -func (e *Entity) NewForwardingEntity(name, comment, email string, config *packet.Config) (forwardeeKey *Entity, proxyParam []byte, err error) { - encryptionSubKey, ok := e.EncryptionKey(config.Now()) - if !ok { - return nil, nil, errors.InvalidArgumentError("no valid encryption key found") - } - - if encryptionSubKey.PublicKey.Version != 4 { - return nil, nil, errors.InvalidArgumentError("unsupported encryption subkey version") - } +// ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) +type ForwardingInstance struct { + ForwarderKeyId uint64 + ForwardeeKeyId uint64 + ProxyParameter []byte +} - if encryptionSubKey.PrivateKey.PubKeyAlgo != packet.PubKeyAlgoECDH { - return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") +// 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 []ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") } - ecdhKey, ok := encryptionSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) - if !ok { - return nil, nil, errors.InvalidArgumentError("encryption subkey is not type ECDH") + now := config.Now() + i := e.PrimaryIdentity() + if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired + i.SelfSignature == nil || // user ID has no self-signature + 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() - forwardeeKey, err = NewEntity(name, comment, email, config) + forwardeePrimaryPrivRaw, err := newSigner(config) if err != nil { return nil, nil, err } - forwardeeEcdhKey, ok := forwardeeKey.Subkeys[0].PrivateKey.PrivateKey.(*ecdh.PrivateKey) - if !ok { - return nil, nil, goerrors.New("wrong forwarding sub key generation") + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, } - proxyParam, err = ecdh.DeriveProxyParam(ecdhKey, forwardeeEcdhKey) + err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs) if err != nil { return nil, nil, err } - kdf := ecdh.KDF{ - Version: ecdh.KDFVersionForwarding, - Hash: ecdhKey.KDF.Hash, - Cipher: ecdhKey.KDF.Cipher, - ReplacementFingerprint: encryptionSubKey.PublicKey.Fingerprint, + // Init empty instances + instances = []ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.Sig.FlagsValid || forwarderSubKey.Sig.FlagCertify || forwarderSubKey.Sig.FlagSign || + forwarderSubKey.Sig.FlagAuthenticate || forwarderSubKey.Sig.FlagGroupKey { + 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 := ForwardingInstance{ + ForwarderKeyId: forwarderSubKey.PublicKey.KeyId, + } + + 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 + } + + // Set ID after changing the KDF + instance.ForwardeeKeyId = forwardeeSubKey.PublicKey.KeyId + + // 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 + + // Append each valid instance to the list + instances = append(instances, instance) } - err = forwardeeKey.Subkeys[0].PublicKey.ReplaceKDF(kdf) - if err != nil { - return nil, nil, err + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") } - // 0x04 - This key may be used to encrypt communications. - forwardeeKey.Subkeys[0].Sig.FlagEncryptCommunications = false - - // 0x08 - This key may be used to encrypt storage. - forwardeeKey.Subkeys[0].Sig.FlagEncryptStorage = false - - // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. - forwardeeKey.Subkeys[0].Sig.FlagSplitKey = true - - // 0x40 - This key may be used for forwarded communications. - forwardeeKey.Subkeys[0].Sig.FlagForward = true - - return forwardeeKey, proxyParam, nil + return forwardeeKey, instances, nil } diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index 1307b1c7..e32e9d51 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -78,11 +78,23 @@ func TestForwardingFull(t *testing.T) { t.Fatal(err) } - charlesEntity, proxyParam, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig) + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) if err != nil { t.Fatal(err) } + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if instances[0].ForwarderKeyId != bobEntity.Subkeys[0].PublicKey.KeyId { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.KeyId, instances[0].ForwarderKeyId) + } + + if instances[0].ForwardeeKeyId != charlesEntity.Subkeys[0].PublicKey.KeyId { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.KeyId, instances[0].ForwardeeKeyId) + } + // Encrypt message buf := bytes.NewBuffer(nil) w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil) @@ -114,6 +126,43 @@ func TestForwardingFull(t *testing.T) { } // 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.Compare(dec, plaintext) != 0 { + 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) + } + + 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.Compare(dec, plaintext) != 0 { + t.Fatal("forwarded decrypted does not match original") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance ForwardingInstance) []byte { bytesReader := bytes.NewReader(encrypted) packets := packet.NewReader(bytesReader) splitPoint := int64(0) @@ -131,9 +180,9 @@ Loop: switch p := p.(type) { case *packet.EncryptedKey: err = p.ProxyTransform( - proxyParam, - charlesEntity.Subkeys[0].PublicKey.KeyId, - bobEntity.Subkeys[0].PublicKey.KeyId, + instance.ProxyParameter, + instance.ForwarderKeyId, + instance.ForwardeeKeyId, ) if err != nil { t.Fatalf("error transforming PKESK: %s", err) @@ -152,15 +201,5 @@ Loop: transformed := transformedEncryptedKey.Bytes() transformed = append(transformed, encrypted[splitPoint:]...) - // 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.Compare(dec, plaintext) != 0 { - t.Fatal("forwarded decrypted does not match original") - } + return transformed } diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 7dada811..03d92d93 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -463,12 +463,12 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } -func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwardeeKeyId, forwardingKeyId uint64) error { +func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwarderKeyId, forwardeeKeyId uint64) error { if e.Algo != PubKeyAlgoECDH { return errors.InvalidArgumentError("invalid PKESK") } - if e.KeyId != 0 && e.KeyId != forwardingKeyId { + if e.KeyId != 0 && e.KeyId != forwarderKeyId { return errors.InvalidArgumentError("invalid key id in PKESK") } From 34e4fe1eabfa76b293cdc0e40ca891227ff60965 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 21 Mar 2023 10:20:39 +0100 Subject: [PATCH 09/36] Resign keys and relax flag requirements --- openpgp/forwarding.go | 10 +++++++--- openpgp/forwarding_test.go | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index 3e447782..c201b03d 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -33,7 +33,6 @@ func (e *Entity) NewForwardingEntity( now := config.Now() i := e.PrimaryIdentity() if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired - i.SelfSignature == nil || // user ID has no self-signature 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 @@ -70,8 +69,7 @@ func (e *Entity) NewForwardingEntity( // Handle all forwarder subkeys for _, forwarderSubKey := range e.Subkeys { // Filter flags - if !forwarderSubKey.Sig.FlagsValid || forwarderSubKey.Sig.FlagCertify || forwarderSubKey.Sig.FlagSign || - forwarderSubKey.Sig.FlagAuthenticate || forwarderSubKey.Sig.FlagGroupKey { + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { continue } @@ -152,6 +150,12 @@ func (e *Entity) NewForwardingEntity( // 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) } diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index e32e9d51..2267c4ca 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -83,6 +83,8 @@ func TestForwardingFull(t *testing.T) { t.Fatal(err) } + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + if len(instances) != 1 { t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) } @@ -147,6 +149,8 @@ func TestForwardingFull(t *testing.T) { t.Fatal(err) } + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) // Decrypt forwarded message for Charles @@ -203,3 +207,21 @@ Loop: 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] +} From d86ac43ace8f738f9dd65f78cacb6b1ccd2092dd Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 21 Mar 2023 15:23:04 +0100 Subject: [PATCH 10/36] Create a copy of the encrypted key when forwarding --- openpgp/forwarding_test.go | 4 ++-- openpgp/packet/encrypted_key.go | 28 +++++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index 2267c4ca..3241a794 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -183,7 +183,7 @@ Loop: } switch p := p.(type) { case *packet.EncryptedKey: - err = p.ProxyTransform( + tp, err := p.ProxyTransform( instance.ProxyParameter, instance.ForwarderKeyId, instance.ForwardeeKeyId, @@ -194,7 +194,7 @@ Loop: splitPoint = bytesReader.Size() - int64(bytesReader.Len()) - err = p.Serialize(transformedEncryptedKey) + err = tp.Serialize(transformedEncryptedKey) if err != nil { t.Fatalf("error serializing transformed PKESK: %s", err) } diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 03d92d93..67fa7316 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -463,27 +463,37 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } -func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwarderKeyId, forwardeeKeyId uint64) error { +func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwarderKeyId, forwardeeKeyId uint64) (transformed *EncryptedKey, err error) { if e.Algo != PubKeyAlgoECDH { - return errors.InvalidArgumentError("invalid PKESK") + return nil, errors.InvalidArgumentError("invalid PKESK") } if e.KeyId != 0 && e.KeyId != forwarderKeyId { - return errors.InvalidArgumentError("invalid key id in PKESK") + return nil, errors.InvalidArgumentError("invalid key id in PKESK") } ephemeral := e.encryptedMPI1.Bytes() - transformed, err := ecdh.ProxyTransform(ephemeral, proxyParam) + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, proxyParam) if err != nil { - return err + return nil, err } - e.encryptedMPI1 = encoding.NewMPI(transformed) - if e.KeyId != 0 { - e.KeyId = forwardeeKeyId + wrappedKey := e.encryptedMPI2.Bytes() + copiedWrappedKey := make([]byte, len(wrappedKey)) + copy(copiedWrappedKey, wrappedKey) + + transformed = &EncryptedKey{ + KeyId: forwardeeKeyId, + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), } - return nil + if e.KeyId == 0 { + e.KeyId = 0 + } + + return transformed, nil } func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { From 3a0e6accf04d9a95c6bbc58b5dbf8525bf5f50d8 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Wed, 22 Mar 2023 10:27:55 +0100 Subject: [PATCH 11/36] Use fingerprints instead of KeyIDs --- openpgp/forwarding.go | 20 +++++--------- openpgp/forwarding_test.go | 16 +++++------ openpgp/packet/encrypted_key.go | 47 ++++++++++++++++++++++++++------- openpgp/packet/forwarding.go | 36 +++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 openpgp/packet/forwarding.go diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index c201b03d..d4291f8e 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -11,20 +11,13 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/packet" ) -// ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) -type ForwardingInstance struct { - ForwarderKeyId uint64 - ForwardeeKeyId uint64 - ProxyParameter []byte -} - // 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 []ForwardingInstance, err error, + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, ) { if e.PrimaryKey.Version != 4 { return nil, nil, errors.InvalidArgumentError("unsupported key version") @@ -64,7 +57,7 @@ func (e *Entity) NewForwardingEntity( } // Init empty instances - instances = []ForwardingInstance{} + instances = []packet.ForwardingInstance{} // Handle all forwarder subkeys for _, forwarderSubKey := range e.Subkeys { @@ -105,8 +98,9 @@ func (e *Entity) NewForwardingEntity( return nil, nil, goerrors.New("wrong forwarding sub key generation") } - instance := ForwardingInstance{ - ForwarderKeyId: forwarderSubKey.PublicKey.KeyId, + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, } instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) @@ -135,8 +129,8 @@ func (e *Entity) NewForwardingEntity( return nil, nil, err } - // Set ID after changing the KDF - instance.ForwardeeKeyId = forwardeeSubKey.PublicKey.KeyId + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint // 0x04 - This key may be used to encrypt communications. forwardeeSubKey.Sig.FlagEncryptCommunications = false diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index 3241a794..c03dd8c5 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -89,12 +89,12 @@ func TestForwardingFull(t *testing.T) { t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) } - if instances[0].ForwarderKeyId != bobEntity.Subkeys[0].PublicKey.KeyId { - t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.KeyId, instances[0].ForwarderKeyId) + if bytes.Compare(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) != 0 { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) } - if instances[0].ForwardeeKeyId != charlesEntity.Subkeys[0].PublicKey.KeyId { - t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.KeyId, instances[0].ForwardeeKeyId) + if bytes.Compare(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) != 0 { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) } // Encrypt message @@ -166,7 +166,7 @@ func TestForwardingFull(t *testing.T) { } } -func transformTestMessage(t *testing.T, encrypted []byte, instance ForwardingInstance) []byte { +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { bytesReader := bytes.NewReader(encrypted) packets := packet.NewReader(bytesReader) splitPoint := int64(0) @@ -183,11 +183,7 @@ Loop: } switch p := p.(type) { case *packet.EncryptedKey: - tp, err := p.ProxyTransform( - instance.ProxyParameter, - instance.ForwarderKeyId, - instance.ForwardeeKeyId, - ) + tp, err := p.ProxyTransform(instance) if err != nil { t.Fatalf("error transforming PKESK: %s", err) } diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 67fa7316..069b57af 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -463,17 +463,17 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } -func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwarderKeyId, forwardeeKeyId uint64) (transformed *EncryptedKey, err error) { +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 != forwarderKeyId { + 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, proxyParam) + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, instance.ProxyParameter) if err != nil { return nil, err } @@ -483,16 +483,12 @@ func (e *EncryptedKey) ProxyTransform(proxyParam []byte, forwarderKeyId, forward copy(copiedWrappedKey, wrappedKey) transformed = &EncryptedKey{ - KeyId: forwardeeKeyId, - Algo: e.Algo, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, encryptedMPI1: encoding.NewMPI(transformedEphemeral), encryptedMPI2: encoding.NewOID(copiedWrappedKey), } - if e.KeyId == 0 { - e.KeyId = 0 - } - return transformed, nil } @@ -641,27 +637,60 @@ func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header [10]byte, pub return err } +<<<<<<< HEAD func checksumKeyMaterial(key []byte) uint16 { var checksum uint16 for _, v := range key { checksum += uint16(v) +======= +func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { + if e.Algo != PubKeyAlgoECDH { + return nil, errors.InvalidArgumentError("invalid PKESK") +>>>>>>> edf1961 (Use fingerprints instead of KeyIDs) } return checksum } +<<<<<<< HEAD func decodeChecksumKey(msg []byte) (key []byte, err error) { key = msg[:len(msg)-2] expectedChecksum := uint16(msg[len(msg)-2])<<8 | uint16(msg[len(msg)-1]) checksum := checksumKeyMaterial(key) if checksum != expectedChecksum { err = errors.StructuralError("session key checksum is incorrect") +======= + if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { + return nil, errors.InvalidArgumentError("invalid key id in PKESK") +>>>>>>> edf1961 (Use fingerprints instead of KeyIDs) } return } +<<<<<<< HEAD func encodeChecksumKey(buffer []byte, key []byte) { copy(buffer, key) checksum := checksumKeyMaterial(key) buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } +======= + 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{ + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), + } + + return transformed, nil +} +>>>>>>> edf1961 (Use fingerprints instead of KeyIDs) diff --git a/openpgp/packet/forwarding.go b/openpgp/packet/forwarding.go new file mode 100644 index 00000000..50b4de44 --- /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") + } +} \ No newline at end of file From 53b20e918e027562e91b1101ad8af6d2b7b0cf2b Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 15 Sep 2023 12:44:22 +0200 Subject: [PATCH 12/36] fix: Address rebase on version 2 issues --- openpgp/forwarding.go | 13 +++++----- openpgp/packet/encrypted_key.go | 46 +++++---------------------------- openpgp/packet/private_key.go | 5 +--- 3 files changed, 15 insertions(+), 49 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index d4291f8e..ae45c3c2 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -6,6 +6,7 @@ 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" @@ -51,7 +52,7 @@ func (e *Entity) NewForwardingEntity( Subkeys: []Subkey{}, } - err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs) + err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs, true) if err != nil { return nil, nil, err } @@ -91,7 +92,7 @@ func (e *Entity) NewForwardingEntity( return nil, nil, err } - forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys) - 1] + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) if !ok { @@ -99,7 +100,7 @@ func (e *Entity) NewForwardingEntity( } instance := packet.ForwardingInstance{ - KeyVersion: 4, + KeyVersion: 4, ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, } @@ -109,9 +110,9 @@ func (e *Entity) NewForwardingEntity( } kdf := ecdh.KDF{ - Version: ecdh.KDFVersionForwarding, - Hash: forwarderEcdhKey.KDF.Hash, - Cipher: forwarderEcdhKey.KDF.Cipher, + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, } // If deriving a forwarding key from a forwarding key diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 069b57af..051d92ab 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -410,7 +410,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 @@ -439,7 +439,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, pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) + 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))) } @@ -483,8 +483,9 @@ func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed copy(copiedWrappedKey, wrappedKey) transformed = &EncryptedKey{ - KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), - Algo: e.Algo, + Version: e.Version, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, encryptedMPI1: encoding.NewMPI(transformedEphemeral), encryptedMPI2: encoding.NewOID(copiedWrappedKey), } @@ -608,7 +609,7 @@ 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 [10]byte, pub *symmetric.AEADPublicKey, keyBlock []byte, config *AEADConfig) error { +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 { @@ -620,7 +621,7 @@ func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header [10]byte, pub buffer := append([]byte{byte(mode)}, iv...) buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) - packetLen := 10 /* header length */ + packetLen := len(header) /* header length */ packetLen += int(len(buffer)) err = serializeHeader(w, packetTypeEncryptedKey, packetLen) @@ -637,60 +638,27 @@ func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header [10]byte, pub return err } -<<<<<<< HEAD func checksumKeyMaterial(key []byte) uint16 { var checksum uint16 for _, v := range key { checksum += uint16(v) -======= -func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { - if e.Algo != PubKeyAlgoECDH { - return nil, errors.InvalidArgumentError("invalid PKESK") ->>>>>>> edf1961 (Use fingerprints instead of KeyIDs) } return checksum } -<<<<<<< HEAD func decodeChecksumKey(msg []byte) (key []byte, err error) { key = msg[:len(msg)-2] expectedChecksum := uint16(msg[len(msg)-2])<<8 | uint16(msg[len(msg)-1]) checksum := checksumKeyMaterial(key) if checksum != expectedChecksum { err = errors.StructuralError("session key checksum is incorrect") -======= - if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { - return nil, errors.InvalidArgumentError("invalid key id in PKESK") ->>>>>>> edf1961 (Use fingerprints instead of KeyIDs) } return } -<<<<<<< HEAD func encodeChecksumKey(buffer []byte, key []byte) { copy(buffer, key) checksum := checksumKeyMaterial(key) buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } -======= - 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{ - KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), - Algo: e.Algo, - encryptedMPI1: encoding.NewMPI(transformedEphemeral), - encryptedMPI2: encoding.NewOID(copiedWrappedKey), - } - - return transformed, nil -} ->>>>>>> edf1961 (Use fingerprints instead of KeyIDs) diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 9dde78ec..406c56e6 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -28,10 +28,10 @@ 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" - "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) // PrivateKey represents a possibly encrypted private key. See RFC 4880, @@ -186,15 +186,12 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewElGamalPublicKey(creationTime, &priv.PublicKey) case *ecdh.PrivateKey: pk.PublicKey = *NewECDHPublicKey(creationTime, &priv.PublicKey) -<<<<<<< HEAD case *x25519.PrivateKey: pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) case *x448.PrivateKey: pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) -======= case *symmetric.AEADPrivateKey: pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) ->>>>>>> 3731c9c (openpgp: Add support for symmetric subkeys (#74)) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } From 5a964bdeafe6523e9dfe4df75da4153e99300972 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 15 Sep 2023 15:13:06 +0200 Subject: [PATCH 13/36] feat: Add forwarding to v2 api --- openpgp/v2/forwarding.go | 159 ++++++++++++++++++++++++ openpgp/v2/forwarding_test.go | 223 ++++++++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 openpgp/v2/forwarding.go create mode 100644 openpgp/v2/forwarding_test.go diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go new file mode 100644 index 00000000..6d1d526b --- /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); err != nil { + return nil, nil, err + } + + // 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(userIdData{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 + } + + forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now) + // 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 = true + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKeySelfSig.FlagEncryptStorage = true + + // 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 00000000..21c8be0e --- /dev/null +++ b/openpgp/v2/forwarding_test.go @@ -0,0 +1,223 @@ +package v2 + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + + "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" + +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 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] +} From a7a9cdc247c1ac4e8b8a6cd423a8d6955a1b09cc Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 15 Sep 2023 15:14:04 +0200 Subject: [PATCH 14/36] fix: Address warnings --- openpgp/forwarding.go | 4 ++-- openpgp/forwarding_test.go | 17 +++++++------- openpgp/keys_test.go | 47 +++++++++++++++++++------------------- openpgp/read_test.go | 2 +- openpgp/write_test.go | 14 +++++------- 5 files changed, 41 insertions(+), 43 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index ae45c3c2..c5622932 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -134,10 +134,10 @@ func (e *Entity) NewForwardingEntity( instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint // 0x04 - This key may be used to encrypt communications. - forwardeeSubKey.Sig.FlagEncryptCommunications = false + forwardeeSubKey.Sig.FlagEncryptCommunications = true // 0x08 - This key may be used to encrypt storage. - forwardeeSubKey.Sig.FlagEncryptStorage = false + forwardeeSubKey.Sig.FlagEncryptStorage = true // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. forwardeeSubKey.Sig.FlagSplitKey = true diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go index c03dd8c5..7bc16718 100644 --- a/openpgp/forwarding_test.go +++ b/openpgp/forwarding_test.go @@ -4,12 +4,13 @@ import ( "bytes" "crypto/rand" goerrors "errors" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "golang.org/x/crypto/openpgp/armor" "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----- @@ -59,7 +60,7 @@ func TestForwardingStatic(t *testing.T) { dec, err := ioutil.ReadAll(m.decrypted) - if bytes.Compare(dec, []byte(forwardedPlaintext)) != 0 { + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { t.Fatal("forwarded decrypted does not match original") } } @@ -89,11 +90,11 @@ func TestForwardingFull(t *testing.T) { t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) } - if bytes.Compare(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) != 0 { + 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.Compare(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) != 0 { + 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) } @@ -123,7 +124,7 @@ func TestForwardingFull(t *testing.T) { } dec, err := ioutil.ReadAll(m.decrypted) - if bytes.Compare(dec, plaintext) != 0 { + if !bytes.Equal(dec, plaintext) { t.Fatal("decrypted does not match original") } @@ -139,7 +140,7 @@ func TestForwardingFull(t *testing.T) { dec, err = ioutil.ReadAll(m.decrypted) - if bytes.Compare(dec, plaintext) != 0 { + if !bytes.Equal(dec, plaintext) { t.Fatal("forwarded decrypted does not match original") } @@ -161,7 +162,7 @@ func TestForwardingFull(t *testing.T) { dec, err = ioutil.ReadAll(m.decrypted) - if bytes.Compare(dec, plaintext) != 0 { + if !bytes.Equal(dec, plaintext) { t.Fatal("forwarded decrypted does not match original") } } diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 26b14571..184325a6 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -19,10 +19,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/symmetric" "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{ @@ -1172,7 +1172,7 @@ func TestAddSubkeySerialized(t *testing.T) { func TestAddHMACSubkey(t *testing.T) { c := &packet.Config{ - RSABits: 512, + RSABits: 512, Algorithm: packet.ExperimentalPubKeyAlgoHMAC, } @@ -1187,7 +1187,7 @@ func TestAddHMACSubkey(t *testing.T) { } buf := bytes.NewBuffer(nil) - w, _ := armor.Encode(buf , "PGP PRIVATE KEY BLOCK", 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) } @@ -1204,37 +1204,36 @@ func TestAddHMACSubkey(t *testing.T) { generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) - if bytes.Compare(parsedPrivateKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { t.Error("parsed wrong key") } - if bytes.Compare(parsedPublicKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { t.Error("parsed wrong key in public part") } - if bytes.Compare(generatedPublicKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { t.Error("generated Public and Private Key differ") } - if bytes.Compare(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) != 0 { + 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.Compare(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) != 0 { + 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}) + 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) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) entity.PrimaryKey.PubKeyAlgo = 100 err = entity.Serialize(w) @@ -1251,7 +1250,7 @@ func TestSerializeSymmetricSubkeyError(t *testing.T) { func TestAddAEADSubkey(t *testing.T) { c := &packet.Config{ - RSABits: 512, + RSABits: 512, Algorithm: packet.ExperimentalPubKeyAlgoAEAD, } entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) @@ -1267,7 +1266,7 @@ func TestAddAEADSubkey(t *testing.T) { generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) buf := bytes.NewBuffer(nil) - w, _ := armor.Encode(buf , "PGP PRIVATE KEY BLOCK", 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) } @@ -1283,39 +1282,39 @@ func TestAddAEADSubkey(t *testing.T) { generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) - if bytes.Compare(parsedPrivateKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { t.Error("parsed wrong key") } - if bytes.Compare(parsedPublicKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { t.Error("parsed wrong key in public part") } - if bytes.Compare(generatedPublicKey.Key, generatedPrivateKey.Key) != 0 { + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { t.Error("generated Public and Private Key differ") } - if bytes.Compare(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) != 0 { + 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.Compare(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) != 0 { + 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, + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, DefaultCipher: packet.CipherAES256, } hmacConfig := &packet.Config{ - RSABits: 512, - DefaultHash: crypto.SHA512, - Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + 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}) diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 78baa19c..99c390bd 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -757,7 +757,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/write_test.go b/openpgp/write_test.go index 3cd03d85..e2b8acb0 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -266,13 +266,13 @@ func TestNewEntity(t *testing.T) { func TestEncryptWithAEAD(t *testing.T) { c := &packet.Config{ - Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + 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}) + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) if err != nil { t.Fatal(err) } @@ -282,8 +282,7 @@ func TestEncryptWithAEAD(t *testing.T) { t.Fatal(err) } - var list []*Entity - list = make([]*Entity, 1) + list := make([]*Entity, 1) list[0] = entity entityList := EntityList(list) buf := bytes.NewBuffer(nil) @@ -308,7 +307,7 @@ func TestEncryptWithAEAD(t *testing.T) { } dec, err := ioutil.ReadAll(m.decrypted) - if bytes.Compare(dec, []byte(message)) != 0 { + if !bytes.Equal(dec, []byte(message)) { t.Error("decrypted does not match original") } } @@ -318,7 +317,7 @@ func TestSignWithHMAC(t *testing.T) { Algorithm: packet.ExperimentalPubKeyAlgoHMAC, DefaultHash: crypto.SHA512, } - entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{ RSABits: 1024}) + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) if err != nil { t.Fatal(err) } @@ -327,8 +326,7 @@ func TestSignWithHMAC(t *testing.T) { if err != nil { t.Fatal(err) } - var list []*Entity - list = make([]*Entity, 1) + list := make([]*Entity, 1) list[0] = entity entityList := EntityList(list) From 972ccd80d37d8912e870f7db2d826d9ae79873bf Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 15 Sep 2023 16:07:30 +0200 Subject: [PATCH 15/36] feat: Add symmetric keys to v2 --- openpgp/forwarding.go | 4 +- openpgp/packet/encrypted_key.go | 2 +- openpgp/v2/forwarding.go | 4 +- openpgp/v2/key_generation.go | 7 + openpgp/v2/keys.go | 18 ++- 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 +++++++++++ 10 files changed, 359 insertions(+), 8 deletions(-) diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go index c5622932..ae45c3c2 100644 --- a/openpgp/forwarding.go +++ b/openpgp/forwarding.go @@ -134,10 +134,10 @@ func (e *Entity) NewForwardingEntity( instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint // 0x04 - This key may be used to encrypt communications. - forwardeeSubKey.Sig.FlagEncryptCommunications = true + forwardeeSubKey.Sig.FlagEncryptCommunications = false // 0x08 - This key may be used to encrypt storage. - forwardeeSubKey.Sig.FlagEncryptStorage = true + forwardeeSubKey.Sig.FlagEncryptStorage = false // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. forwardeeSubKey.Sig.FlagSplitKey = true diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 051d92ab..a84cf6b4 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -223,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]) diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go index 6d1d526b..6d6498c6 100644 --- a/openpgp/v2/forwarding.go +++ b/openpgp/v2/forwarding.go @@ -131,10 +131,10 @@ func (e *Entity) NewForwardingEntity( instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint // 0x04 - This key may be used to encrypt communications. - forwardeeSubKeySelfSig.FlagEncryptCommunications = true + forwardeeSubKeySelfSig.FlagEncryptCommunications = false // 0x08 - This key may be used to encrypt storage. - forwardeeSubKeySelfSig.FlagEncryptStorage = true + forwardeeSubKeySelfSig.FlagEncryptStorage = false // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. forwardeeSubKeySelfSig.FlagSplitKey = true diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 5537d4f8..a5f41391 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" ) @@ -387,6 +388,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") } @@ -429,6 +433,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/v2/keys.go b/openpgp/v2/keys.go index b4a7cc1e..b4924793 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -61,7 +61,7 @@ func (e *Entity) PrimaryIdentity(date time.Time, config *packet.Config) (*packet var primaryIdentityCandidatesSelfSigs []*packet.Signature for _, identity := range e.Identities { selfSig, err := identity.Verify(date, config) // identity must be valid at date - if err == nil { // verification is successful + if err == nil { // verification is successful primaryIdentityCandidates = append(primaryIdentityCandidates, identity) primaryIdentityCandidatesSelfSigs = append(primaryIdentityCandidatesSelfSigs, selfSig) } @@ -608,6 +608,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 } @@ -628,6 +632,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. + subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{}) + if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + (err == nil && subKeySelfSig.FlagForward) { + continue + } if err := subkey.Serialize(w, false); err != nil { return err } @@ -784,5 +798,5 @@ func isValidCertificationKey(signature *packet.Signature, algo packet.PublicKeyA func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { return algo.CanEncrypt() && signature.FlagsValid && - (signature.FlagEncryptCommunications || signature.FlagEncryptStorage) + (signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage) } diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index 0b276c23..c9d27734 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 24c8c8f0..b275130d 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 2feaf392..d7084b8a 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -1002,3 +1002,10 @@ func testMalformedMessage(t *testing.T, keyring EntityList, message string) { return } } + +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 2f0efc22..bb383b19 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -740,3 +740,21 @@ NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91 xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE= =miES -----END PGP PRIVATE KEY BLOCK-----` + +// 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 f3c4f9da..d3c7ff48 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) + } +} From 66300b5308d842b99b3a27955e946843666a3c64 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Tue, 26 Sep 2023 15:03:48 +0200 Subject: [PATCH 16/36] fix(v2): Do not allow encrpytion with a forwarding key --- openpgp/v2/keys.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index b4924793..40832ed6 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -163,12 +163,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 && - isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && + isValidDecryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && (id == 0 || subkey.PublicKey.KeyId == id) { keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, subkeySelfSig}) } } - if isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { + if isValidDecryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) } return @@ -796,6 +796,12 @@ func isValidCertificationKey(signature *packet.Signature, algo packet.PublicKeyA } func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanEncrypt() && + 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 8815d5b1723f6c7a6764956865d27c6ab02fcb35 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 5 Oct 2023 10:28:46 +0200 Subject: [PATCH 17/36] fix(v2): Adapt NewForwardingEntity to refactored NewEntity --- openpgp/v2/forwarding.go | 8 ++++---- openpgp/v2/keys.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go index 6d6498c6..1306c510 100644 --- a/openpgp/v2/forwarding.go +++ b/openpgp/v2/forwarding.go @@ -26,14 +26,13 @@ func (e *Entity) NewForwardingEntity( now := config.Now() - if _, err = e.VerifyPrimaryKey(now); err != nil { + 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 - keyLifetimeSecs := config.KeyLifetime() forwardeePrimaryPrivRaw, err := newSigner(config) if err != nil { @@ -49,7 +48,8 @@ func (e *Entity) NewForwardingEntity( Subkeys: []Subkey{}, } - err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, now, keyLifetimeSecs, true) + keyProperties := selectKeyProperties(now, config, primary) + err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, keyProperties) if err != nil { return nil, nil, err } @@ -64,7 +64,7 @@ func (e *Entity) NewForwardingEntity( continue } - forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now) + forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now, config) // Filter expiration & revokal if err != nil { continue diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index 40832ed6..192ebbaf 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -636,7 +636,7 @@ func (e *Entity) Serialize(w io.Writer) error { // 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{}) + subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{}, nil) if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || (err == nil && subKeySelfSig.FlagForward) { From e7d584c00bdc10635cf35a28a9fa020f09dda8c1 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 18 Jan 2024 15:33:40 +0100 Subject: [PATCH 18/36] 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 d3c7ff48..28550862 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 e2b8acb0..315e7323 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 e68b81831ffbe3e2edee2a1847cc26da4fccfdf5 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Thu, 14 Mar 2024 12:31:10 +0100 Subject: [PATCH 19/36] Fix HMAC generation (#204) Generate an AEAD subkey when requesting an HMAC primary key. --- openpgp/v2/key_generation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index a5f41391..3029fae7 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -433,7 +433,7 @@ 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) default: From d537e9529ce149980dc03dac2ed250b7b47cc833 Mon Sep 17 00:00:00 2001 From: Aron Wussler Date: Tue, 19 Jul 2022 12:41:32 +0200 Subject: [PATCH 20/36] Full PQC support (+12 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 --- go.mod | 7 +- go.sum | 9 + openpgp/benchmark_v6_test.go | 355 ++++++++++++++++++++ openpgp/internal/ecc/curves.go | 2 + openpgp/internal/ecc/generic.go | 9 + openpgp/internal/encoding/octetarray.go | 65 ++++ openpgp/key_generation.go | 71 +++- openpgp/keys.go | 24 +- openpgp/keys_test.go | 88 +++++ openpgp/keys_test_data.go | 391 ++++++++++++++++++++++ openpgp/keys_v6_test.go | 155 +++++++++ openpgp/mldsa_ecdsa/mldsa_ecdsa.go | 118 +++++++ openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go | 93 ++++++ openpgp/mldsa_eddsa/mldsa_eddsa.go | 85 +++++ openpgp/mldsa_eddsa/mldsa_eddsa_test.go | 90 ++++++ openpgp/mlkem_ecdh/mlkem_ecdh.go | 242 ++++++++++++++ openpgp/mlkem_ecdh/mlkem_ecdh_test.go | 107 ++++++ openpgp/packet/config.go | 10 + openpgp/packet/encrypted_key.go | 119 +++++-- openpgp/packet/packet.go | 27 +- openpgp/packet/private_key.go | 220 ++++++++++++- openpgp/packet/public_key.go | 411 +++++++++++++++++++++++- openpgp/packet/signature.go | 152 ++++++++- openpgp/pqc_vectors_test.go | 217 +++++++++++++ openpgp/read.go | 8 +- openpgp/read_test.go | 99 ++++++ openpgp/read_write_test_data.go | 374 +++++++++++++++++++++ openpgp/slhdsa/parameter.go | 85 +++++ openpgp/slhdsa/sphincs.go | 155 +++++++++ openpgp/slhdsa/sphincs_test.go | 124 +++++++ openpgp/v2/write.go | 1 + openpgp/write_test.go | 300 +++++++++++------ 32 files changed, 4071 insertions(+), 142 deletions(-) create mode 100644 openpgp/benchmark_v6_test.go create mode 100644 openpgp/internal/encoding/octetarray.go create mode 100644 openpgp/mldsa_ecdsa/mldsa_ecdsa.go create mode 100644 openpgp/mldsa_ecdsa/mldsa_ecdsa_test.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 create mode 100644 openpgp/pqc_vectors_test.go create mode 100644 openpgp/slhdsa/parameter.go create mode 100644 openpgp/slhdsa/sphincs.go create mode 100644 openpgp/slhdsa/sphincs_test.go diff --git a/go.mod b/go.mod index d417da35..a0b64190 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,15 @@ module github.com/ProtonMail/go-crypto -go 1.17 +go 1.21 + +toolchain go1.22.0 require ( github.com/cloudflare/circl v1.3.7 + github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c golang.org/x/crypto v0.17.0 ) require golang.org/x/sys v0.16.0 // indirect + +replace github.com/cloudflare/circl v1.3.7 => github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2 diff --git a/go.sum b/go.sum index 712b2d44..22bf1b54 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,22 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v0.0.0-20240227155846-ede8f45b4d37/go.mod h1:Rtp2DgaIOIqDrWkeSBF4qtj92/5YQzSwE4QRH+px1bs= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c h1:JCxCKz59IXghzSSUstoaWa7h7lZdmd0LFMiMfF56ECk= +github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c/go.mod h1:+SeUKO8dPlXRdYr4SK+UIs8SLz0Dl3ZceKdXGaSFsFY= +github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2 h1:b1oBEyYCXXr5y+OxdGmXbKuKlm526nhJnPFmJ0pGFGs= +github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 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.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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= @@ -18,6 +25,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ 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-20210423082822-04245dca01da/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= @@ -33,6 +41,7 @@ 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.6/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= diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go new file mode 100644 index 00000000..ec3db054 --- /dev/null +++ b/openpgp/benchmark_v6_test.go @@ -0,0 +1,355 @@ +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, + }, + "ML-DSA3P256_ML-KEM768P256": { + Algorithm: packet.PubKeyAlgoMldsa65p256, + }, + "ML-DSA5P384_ML-KEM1024P384": { + Algorithm: packet.PubKeyAlgoMldsa87p384, + }, + "ML-DSA3Brainpool256_ML-KEM768Brainpool256": { + Algorithm: packet.PubKeyAlgoMldsa65Brainpool256, + }, + "ML-DSA5Brainpool384_ML-KEM1024Brainpool384": { + Algorithm: packet.PubKeyAlgoMldsa87Brainpool384, + }, + "SLH-DSA-SHA2_128s_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 1, + }, + "SLH-DSA-SHA2_128f_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 2, + }, + "SLH-DSA-SHA2_192s_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 3, + }, + "SLH-DSA-SHA2_192f_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 4, + }, + "SLH-DSA-SHA2_256s_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 5, + }, + "SLH-DSA-SHA2_256f_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoSlhdsaSha2, + SlhdsaParameterId: 6, + }, + "SLH-DSA-SHAKE_128s_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 1, + }, + "SLH-DSA-SHAKE_128f_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 2, + }, + "SLH-DSA-SHAKE_192s_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 3, + }, + "SLH-DSA-SHAKE_192f_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 4, + }, + "SLH-DSA-SHAKE_256s_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 5, + }, + "SLH-DSA-SHAKE_256f_ML-KEM1024X448":{ + Algorithm: packet.PubKeyAlgoSlhdsaShake, + SlhdsaParameterId: 6, + }, +} + +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/internal/ecc/curves.go b/openpgp/internal/ecc/curves.go index 5ed9c93b..34c4ad86 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 e28d7c71..e50c532f 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 00000000..ac6d6bb1 --- /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 bcf23175..de801158 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -13,6 +13,10 @@ import ( "math/big" "time" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -21,6 +25,7 @@ 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/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" @@ -312,6 +317,49 @@ 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.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, + packet.PubKeyAlgoMldsa87Brainpool384: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 ML-DSA + ECDSA key") + } + + c, err := packet.GetEcdsaCurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_ecdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) + 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) + case packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSA key") + } + + mode, err := packet.GetSlhdsaModeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + parameter := config.SlhdsaParam() + + return slhdsa.GenerateKey(config.Random(), mode, parameter) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -319,7 +367,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 { @@ -357,6 +406,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, packet.PubKeyAlgoMldsa65p256, + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, + packet.PubKeyAlgoMldsa87Brainpool384, packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + if pubKeyAlgo, err = packet.GetMatchingMlkemKem(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, packet.PubKeyAlgoMlkem768P256, + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + + 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 bbcc95d9..131c30b4 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,21 @@ 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 { + switch s.PublicKey.PubKeyAlgo { + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384, + packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + return true + default: + return false + } + +} + // 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 184325a6..1734cd24 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2085,3 +2085,91 @@ TxGVotQ4A/0u0VbOMEUfnrI8Fms= t.Errorf("Expected AEAD subkey") } } +func TestAddV4MlkemSubkey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEdDSA, + V6Keys: false, + DefaultCipher: packet.CipherAES256, + 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, false) +} + +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, + "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, + "Mlkem1024_P384": packet.PubKeyAlgoMlkem1024P384, + "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, + "Mlkem1024_Brainpool384": packet.PubKeyAlgoMlkem1024Brainpool384, + } + + 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_test_data.go b/openpgp/keys_test_data.go index 108fd096..82481ac9 100644 --- a/openpgp/keys_test_data.go +++ b/openpgp/keys_test_data.go @@ -536,3 +536,394 @@ VppQxdtxPvAA/34snHBX7Twnip1nMt7P4e2hDiw/hwQ7oqioOvc6jMkP =Z8YJ -----END PGP PRIVATE KEY BLOCK----- ` +const eddsaMlkem512X25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +ByIM +=camV +-----END PGP PRIVATE KEY BLOCK-----` + +const eddsaMlkem1024X448PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +R/YH +=MhqF +-----END PGP PRIVATE KEY BLOCK-----` + +const eddsaMlkem768P384PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=mDQF +-----END PGP PRIVATE KEY BLOCK-----` + +const eddsaMlkem1024P521PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=3UOU +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index fc9ba776..28009667 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -3,8 +3,16 @@ package openpgp import ( "bytes" "crypto" + "crypto/rand" "strings" "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "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/packet" ) @@ -196,3 +204,150 @@ 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, + "ML-DSA65_P256": packet.PubKeyAlgoMldsa65p256, + "ML-DSA87_P384": packet.PubKeyAlgoMldsa87p384, + "ML-DSA65_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, + "ML-DSA87_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, + "Slhdsa_simple_SHA2": packet.PubKeyAlgoSlhdsaSha2, + "Slhdsa_simple_SHAKE": packet.PubKeyAlgoSlhdsaShake, + } + + 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) + } + + // Corrupt public ML-DSA in primary key + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_ecdsa.PublicKey); ok { + bin := pk.PublicMldsa.Bytes() + bin[5] ^= 1 + pk.PublicMldsa = pk.Mldsa.PublicKeyFromBytes(bin) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { + bin := pk.PublicMldsa.Bytes() + bin[5] ^= 1 + pk.PublicMldsa = pk.Mldsa.PublicKeyFromBytes(bin) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*slhdsa.PublicKey); ok { + pk.PublicData.PKseed[5] ^= 1 + } + + 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_ecdsa/mldsa_ecdsa.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa.go new file mode 100644 index 00000000..c5cfd92f --- /dev/null +++ b/openpgp/mldsa_ecdsa/mldsa_ecdsa.go @@ -0,0 +1,118 @@ +// Package mldsa_ecdsa implements hybrid ML-DSA + ECDSA encryption, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-signature-schemes +package mldsa_ecdsa + +import ( + "crypto/subtle" + goerrors "errors" + "github.com/cloudflare/circl/sign/dilithium" + "io" + "math/big" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.ECDSACurve + Mldsa dilithium.Mode + X, Y *big.Int + PublicMldsa dilithium.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc *big.Int + SecretMldsa dilithium.PrivateKey +} + +func (pk *PublicKey) MarshalPoint() []byte { + return pk.Curve.MarshalIntegerPoint(pk.X, pk.Y) +} + +func (pk *PublicKey) UnmarshalPoint(p []byte) error { + pk.X, pk.Y = pk.Curve.UnmarshalIntegerPoint(p) + if pk.X == nil { + return goerrors.New("mldsa_ecdsa: failed to parse EC point") + } + return nil +} + +func (sk *PrivateKey) MarshalIntegerSecret() []byte { + return sk.Curve.MarshalFieldInteger(sk.SecretEc) +} + +func (sk *PrivateKey) UnmarshalIntegerSecret(d []byte) error { + sk.SecretEc = sk.Curve.UnmarshalFieldInteger(d) + + if sk.SecretEc == nil { + return goerrors.New("mldsa_ecdsa: failed to parse scalar") + } + return nil +} + +// GenerateKey generates a ML-DSA + ECDSA composite key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation-procedure-2 +func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDSACurve, d dilithium.Mode) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mldsa = d + + priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc, err = c.GenerateECDSA(rand) + if err != nil { + return nil, err + } + + priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey(rand) + return +} + +// Sign generates a ML-DSA + ECDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-generation +func Sign(rand io.Reader, priv *PrivateKey, message []byte) (dSig, ecR, ecS []byte, err error) { + r, s, err := priv.PublicKey.Curve.Sign(rand, priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc, message) + if err != nil { + return nil, nil, nil, err + } + + ecR = priv.PublicKey.Curve.MarshalFieldInteger(r) + ecS = priv.PublicKey.Curve.MarshalFieldInteger(s) + + dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message) + if dSig == nil { + return nil, nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") + } + + return +} + +// Verify verifies a ML-DSA + ECDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-verification +func Verify(pub *PublicKey, message, dSig, ecR, ecS []byte) bool { + r := pub.Curve.UnmarshalFieldInteger(ecR) + s := pub.Curve.UnmarshalFieldInteger(ecS) + + return pub.Curve.Verify(pub.X, pub.Y, message, r, s) && pub.Mldsa.Verify(pub.PublicMldsa, message, dSig) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateECDSA(priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc.Bytes()); err != nil { + return err + } + + pub := priv.SecretMldsa.Public() + casted, ok := pub.(dilithium.PublicKey) + if !ok { + return errors.KeyInvalidError("mldsa_ecdsa: invalid public key") + } + + if subtle.ConstantTimeCompare(priv.PublicMldsa.Bytes(), casted.Bytes()) == 0 { + return errors.KeyInvalidError("mldsa_ecdsa: invalid public key") + } + + return +} \ No newline at end of file diff --git a/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go new file mode 100644 index 00000000..3dae1f49 --- /dev/null +++ b/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go @@ -0,0 +1,93 @@ +// Package mldsa_ecdsa_test tests the implementation of hybrid ML-DSA + ECDSA encryption, suitable for OpenPGP, experimental. +package mldsa_ecdsa_test + +import ( + "crypto/rand" + "io" + "math/big" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string] packet.PublicKeyAlgorithm { + "ML-DSA3_P256": packet.PubKeyAlgoMldsa65p256, + "ML-DSA5_P384": packet.PubKeyAlgoMldsa87p384, + "ML-DSA3_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, + "ML-DSA5_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, + } + + 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_ecdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin := key.PublicMldsa.Bytes() + bin[5] ^= 1 + key.PublicMldsa = key.Mldsa.PublicKeyFromBytes(bin) + + if err := mldsa_ecdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mldsa_ecdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.X.Sub(key.X, big.NewInt(1)) + if err := mldsa_ecdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_ecdsa.PrivateKey { + curveObj, err := packet.GetEcdsaCurveFromAlgID(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_ecdsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + + +func testSignVerifyAlgo(t *testing.T, priv *mldsa_ecdsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, ecR, ecS, err := mldsa_ecdsa.Sign(rand.Reader, priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := mldsa_ecdsa.Verify(&priv.PublicKey, digest, dSig, ecR, ecS) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go new file mode 100644 index 00000000..812a51c3 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -0,0 +1,85 @@ +// Package mldsa_eddsa implements hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-signature-schemes +package mldsa_eddsa + +import ( + "crypto/subtle" + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/sign/dilithium" +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.EdDSACurve + Mldsa dilithium.Mode + PublicPoint []byte + PublicMldsa dilithium.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMldsa dilithium.PrivateKey +} + +// GenerateKey generates a ML-DSA + EdDSA composite key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation-procedure-2 +func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d dilithium.Mode) (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 + } + + priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey(rand) + return +} + +// Sign generates a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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) + if dSig == nil { + return nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") + } + + return +} + +// Verify verifies a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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) +} + +// 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 + } + + pub := priv.SecretMldsa.Public() + casted, ok := pub.(dilithium.PublicKey) + if !ok { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + + if subtle.ConstantTimeCompare(priv.PublicMldsa.Bytes(), casted.Bytes()) == 0 { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + return +} \ No newline at end of file diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go new file mode 100644 index 00000000..b35b8275 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -0,0 +1,90 @@ +// 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 := key.PublicMldsa.Bytes() + bin[5] ^= 1 + key.PublicMldsa = key.Mldsa.PublicKeyFromBytes(bin) + + 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 00000000..04556181 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -0,0 +1,242 @@ +// Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +// It follows the spec https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-kem-schemes +package mlkem_ecdh + +import ( + goerrors "errors" + "fmt" + "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "golang.org/x/crypto/sha3" + "io" + + "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" +) + +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 +} + +// GenerateKey implements ML-KEM + ECC key generation as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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 + } + + kyberSeed := make([]byte, k.SeedSize()) + _, err = rand.Read(kyberSeed) + if err != nil { + return nil, err + } + + priv.PublicKey.PublicMlkem, priv.SecretMlkem = priv.PublicKey.Mlkem.DeriveKeyPair(kyberSeed) + return +} + +// Encrypt implements ML-KEM + ECC encryption as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-encryption-procedure +func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { + if len(msg) > 64 { + 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 := make([]byte, pub.Mlkem.EncapsulationSeedSize()) + _, err = rand.Read(kyberSeed) + 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 + } + + kek, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS, kEphemeral, pub.PublicMlkem) + if err != nil { + return nil, nil, nil, err + } + + if ciphertext, err = keywrap.Wrap(kek, 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-wussler-openpgp-pqc-03.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 + } + + msg, err = keywrap.Unwrap(kek, ciphertext) + + fmt.Printf("kek:%x\nsk:%x\n", kek, msg) + + return msg, err +} + +// buildKey implements the composite KDF as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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) + + serializedMlkemKey, err := mlkemPublicKey.MarshalBinary() + if err != nil { + return nil, err + } + + // eccData = eccKeyShare || eccCipherText + // mlkemData = mlkemKeyShare || mlkemCipherText + // encData = counter || eccData || mlkemData || fixedInfo + k := sha3.New256() + + // SHA3 never returns error + _, _ = k.Write([]byte{0x00, 0x00, 0x00, 0x01}) + _, _ = k.Write(eccKeyShare) + _, _ = k.Write(eccEphemeral) + _, _ = k.Write(eccPublicKey) + _, _ = k.Write(mlkemKeyShare) + _, _ = k.Write(mlkemEphemeral) + _, _ = k.Write(serializedMlkemKey) + _, _ = k.Write([]byte{pub.AlgId}) + _, _ = k.Write([]byte("OpenPGPCompositeKDFv1")) + + fmt.Printf("ecc:%x\nkyber:%x\n", eccKeyShare, mlkemKeyShare) + + 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 + } + } + + _, err = w.Write(encryptedSessionKey) + return err +} + +// 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 +} \ No newline at end of file diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go new file mode 100644 index 00000000..fb0a0b9a --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -0,0 +1,107 @@ +// 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" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "testing" +) + +func TestEncryptDecrypt(t *testing.T) { + asymmAlgos := map[string] packet.PublicKeyAlgorithm { + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, + "Mlkem1024_P384":packet.PubKeyAlgoMlkem1024P384, + "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, + "Mlkem1024_Brainpool384":packet.PubKeyAlgoMlkem1024Brainpool384, + } + + symmAlgos := map[string] algorithm.Cipher { + "AES-128": algorithm.AES128, + "AES-192": algorithm.AES192, + "AES-256": algorithm.AES256, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + for symmName, symmAlgo := range symmAlgos { + t.Run(symmName, func(t *testing.T) { + testEncryptDecryptAlgo(t, key, symmAlgo) + }) + } + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + var err error + key := testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, _ := key.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + key.PublicMlkem, err = key.Mlkem.UnmarshalBinaryPublicKey(bin) + if err != nil { + t.Fatal("unable to corrupt key") + } + + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_ecdh.PrivateKey { + curveObj, err := packet.GetECDHCurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMlkemFromAlgID(algId) + if err != nil { + t.Errorf("error getting kyber: %s", err) + } + + priv, err := mlkem_ecdh.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testEncryptDecryptAlgo(t *testing.T, priv *mlkem_ecdh.PrivateKey, kdfCipher algorithm.Cipher) { + expectedMessage := make([]byte, kdfCipher.KeySize()) // encryption algo + checksum + rand.Read(expectedMessage) + + kE, ecE, c, err := mlkem_ecdh.Encrypt(rand.Reader, &priv.PublicKey, expectedMessage) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + decryptedMessage, err := mlkem_ecdh.Decrypt(priv, kE, ecE, c) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + if !bytes.Equal(decryptedMessage, expectedMessage) { + t.Errorf("decryption failed, got: %x, want: %x", decryptedMessage, expectedMessage) + } +} diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index fb21e6d1..e52fa55a 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -7,6 +7,7 @@ package packet import ( "crypto" "crypto/rand" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "io" "math/big" "time" @@ -93,6 +94,8 @@ type Config struct { // Curve configures the desired packet.Curve if the Algorithm is PubKeyAlgoECDSA, // PubKeyAlgoEdDSA, or PubKeyAlgoECDH. If empty Curve25519 is used. Curve Curve + // SlhdsaParameterId configures the desired sphincs plus security level parameter. + SlhdsaParameterId slhdsa.ParameterSetId // AEADConfig configures the use of the new AEAD Encrypted Data Packet, // defined in the draft of the next version of the OpenPGP specification. // If a non-nil AEADConfig is passed, usage of this packet is enabled. By @@ -263,6 +266,13 @@ func (c *Config) S2K() *s2k.Config { return c.S2KConfig } +func (c *Config) SlhdsaParam() slhdsa.ParameterSetId { + if c == nil || c.SlhdsaParameterId == 0 { + return slhdsa.Param128f + } + return c.SlhdsaParameterId +} + func (c *Config) AEAD() *AEADConfig { if c == nil { return nil diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index a84cf6b4..c60f8df3 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -14,6 +14,8 @@ import ( "math/big" "strconv" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -35,10 +37,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 +157,38 @@ 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 + } + case PubKeyAlgoMlkem768P256: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 65, 1088, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem1024P384: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 97, 1568, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem768Brainpool256: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 65, 1088, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem1024Brainpool384: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 97, 1568, e.Version == 6); err != nil { + return err + } } if e.Version < 6 { switch e.Algo { - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, + PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, + PubKeyAlgoMlkem1024Brainpool384: e.CipherFunc = CipherFunction(cipherFunction) - // Check for validiy is in the Decrypt method + // Check for validity is in the Decrypt method } } @@ -214,6 +244,13 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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 +270,23 @@ 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, PubKeyAlgoMlkem768P256, + PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: 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 +305,12 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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 +381,10 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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") } @@ -367,13 +415,15 @@ 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, + PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, + PubKeyAlgoMlkem1024Brainpool384: + 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 } } @@ -422,7 +472,8 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, + PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: // algorithm is added in plaintext below keyBlock = key } @@ -440,6 +491,9 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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))) } @@ -662,3 +716,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 dd4ad34c..d7ab7416 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -512,13 +512,33 @@ 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 + PubKeyAlgoMlkem768P256 = 31 + PubKeyAlgoMlkem1024P384 = 32 + PubKeyAlgoMlkem768Brainpool256 = 33 + PubKeyAlgoMlkem1024Brainpool384 = 34 + + // Experimental PQC DSA algorithms + PubKeyAlgoMldsa65Ed25519 = 107 + PubKeyAlgoMldsa87Ed448 = 108 + PubKeyAlgoMldsa65p256 = 37 + PubKeyAlgoMldsa87p384 = 38 + PubKeyAlgoMldsa65Brainpool256 = 39 + PubKeyAlgoMldsa87Brainpool384 = 40 + PubKeyAlgoSlhdsaSha2 = 109 + PubKeyAlgoSlhdsaShake = 42 ) // 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: return true } return false @@ -528,7 +548,10 @@ 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, PubKeyAlgoMldsa65p256, + PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384, + PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 406c56e6..aeff67fe 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -13,12 +13,20 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" + goerrors "errors" "fmt" "io" "math/big" "strconv" "time" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign/dilithium" + "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -27,6 +35,7 @@ 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/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 +178,12 @@ 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_ecdsa.PrivateKey: + pk.PublicKey = *NewMldsaEcdsaPublicKey(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") } @@ -176,7 +191,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 +207,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 +567,53 @@ 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-wussler-openpgp-pqc-03.html#name-key-material-packets +func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err error) { + var kyberBin []byte + if kyberBin, err = priv.SecretMlkem.MarshalBinary(); err != nil { + return err + } + if _, err = w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + _, err = w.Write(encoding.NewOctetArray(kyberBin).EncodedBytes()) + return err +} + +// serializeMldsaEcdsaPrivateKey serializes a ML-DSA + ECDSA private key according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +func serializeMldsaEcdsaPrivateKey(w io.Writer, priv *mldsa_ecdsa.PrivateKey) error { + if _, err := w.Write(encoding.NewOctetArray(priv.MarshalIntegerSecret()).EncodedBytes()); err != nil { + return err + } + _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsa.Bytes()).EncodedBytes()) + return err +} + +// serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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 + } + _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsa.Bytes()).EncodedBytes()) + return err +} + +// serializeSlhdsaPrivateKey serializes a SLH-DSA private key according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 +func serializeSlhdsaPrivateKey(w io.Writer, priv *slhdsa.PrivateKey) error { + privateData, err := priv.SerializePrivate() + if err != nil { + return err + } + + _, err = w.Write(encoding.NewOctetArray(privateData).EncodedBytes()) + return err } // decrypt decrypts an encrypted private key using a decryption key. @@ -857,6 +920,15 @@ 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_ecdsa.PrivateKey: + err = serializeMldsaEcdsaPrivateKey(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") } @@ -885,13 +957,29 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: + return pk.parseMlkemEcdhPrivateKey(data, 32, mlkem768.PrivateKeySize) + case PubKeyAlgoMlkem1024X448: + return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem1024.PrivateKeySize) + case PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: + return pk.parseMlkemEcdhPrivateKey(data, 48, mlkem1024.PrivateKeySize) + case PubKeyAlgoMldsa65Ed25519: + return pk.parseMldsaEddsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) + case PubKeyAlgoMldsa87Ed448: + return pk.parseMldsaEddsaPrivateKey(data, 57, dilithium.MLDSA87.PrivateKeySize()) + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: + return pk.parseMldsaEcdsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) + case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: + return pk.parseMldsaEcdsaPrivateKey(data, 48, dilithium.MLDSA87.PrivateKeySize()) + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + return pk.parseSlhdsaPrivateKey(data) + default: + err = errors.StructuralError("unknown private key type") + return } } @@ -1212,6 +1300,128 @@ func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { return nil } +// parseMldsaEcdsaPrivateKey parses a ML-DSA + ECDSA private key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +func (pk *PrivateKey) parseMldsaEcdsaPrivateKey(data []byte, ecLen, dLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + ECDSA key") + } + pub := pk.PublicKey.PublicKey.(*mldsa_ecdsa.PublicKey) + priv := new(mldsa_ecdsa.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + + d := encoding.NewEmptyOctetArray(dLen) + if _, err := d.ReadFrom(buf); err != nil { + return err + } + + err = priv.UnmarshalIntegerSecret(ec.Bytes()) + if err != nil { + return err + } + + priv.SecretMldsa = priv.Mldsa.PrivateKeyFromBytes(d.Bytes()) + if err := mldsa_ecdsa.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, dLen 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 + } + + d := encoding.NewEmptyOctetArray(dLen) + if _, err := d.ReadFrom(buf); err != nil { + return err + } + + priv.SecretEc = ec.Bytes() + priv.SecretMldsa = priv.Mldsa.PrivateKeyFromBytes(d.Bytes()) + 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-wussler-openpgp-pqc-03.html#name-key-material-packets +func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, kLen int) (err error) { + pub := pk.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey) + priv := new(mlkem_ecdh.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + + k := encoding.NewEmptyOctetArray(kLen) + if _, err := k.ReadFrom(buf); err != nil { + return err + } + + priv.SecretEc = ec.Bytes() + if priv.SecretMlkem, err = priv.PublicKey.Mlkem.UnmarshalBinaryPrivateKey(k.Bytes()); err != nil { + return err + } + + if err := mlkem_ecdh.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseSlhdsaPrivateKey parses a SLH-DSA private key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 +func (pk *PrivateKey) parseSlhdsaPrivateKey(data []byte) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 SLH-DSA key") + } + pub := pk.PublicKey.PublicKey.(*slhdsa.PublicKey) + priv := new(slhdsa.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + spx := encoding.NewEmptyOctetArray(priv.ParameterSetId.GetSkLen()) + if _, err := spx.ReadFrom(buf); err != nil { + return err + } + + priv.UnmarshalPrivate(spx.Bytes()) + if err := slhdsa.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 5b16eba9..08a4947f 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -7,6 +7,7 @@ package packet import ( "bytes" "crypto/dsa" + "crypto/elliptic" "crypto/rsa" "crypto/sha1" "crypto/sha256" @@ -20,6 +21,15 @@ import ( "strconv" "time" + "github.com/ProtonMail/go-crypto/brainpool" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/cloudflare/circl/kem" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign/dilithium" + "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -30,6 +40,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/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" @@ -40,7 +51,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 @@ -55,6 +66,9 @@ type PublicKey struct { // kdf stores key derivation function parameters // used for ECDH encryption. See RFC 6637, Section 9. kdf encoding.Field + + // slhDsaParameterSetId contains the parameter set ID for the SLH-DSA instantiation + slhDsaParameterSetId slhdsa.ParameterSetId } // UpgradeToV5 updates the version of the key to v5, and updates all necessary @@ -253,8 +267,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, @@ -265,8 +278,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, @@ -276,6 +288,74 @@ 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: 4, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(mlkemBin), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewMldsaEcdsaPublicKey(creationTime time.Time, pub *mldsa_ecdsa.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.MarshalPoint()), + q: encoding.NewOctetArray(pub.PublicMldsa.Bytes()), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) *PublicKey { + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(pub.PublicMldsa.Bytes()), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKey { + var pk *PublicKey + + publicData, err := pub.SerializePublic() + if err != nil { + panic("generated invalid SLH-DSA public key") + } + + pk = &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: GetAlgIDFromSlhdsaMode(pub.Mode), + PublicKey: pub, + p: encoding.NewOctetArray(publicData), + slhDsaParameterSetId: pub.ParameterSetId, + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -304,7 +384,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) @@ -330,6 +410,26 @@ 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 PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: + err = pk.parseMlkemEcdh(r, 65, mlkem768.PublicKeySize) + case PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: + err = pk.parseMlkemEcdh(r, 97, mlkem1024.PublicKeySize) + case PubKeyAlgoMldsa65Ed25519: + err = pk.parseMldsaEddsa(r, 32, dilithium.MLDSA65.PublicKeySize()) + case PubKeyAlgoMldsa87Ed448: + err = pk.parseMldsaEddsa(r, 57, dilithium.MLDSA87.PublicKeySize()) + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: + err = pk.parseMldsaEcdsa(r, 65, dilithium.MLDSA65.PublicKeySize()) + case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: + err = pk.parseMldsaEcdsa(r, 97, dilithium.MLDSA87.PublicKeySize()) + case PubKeyAlgoSlhdsaSha2: + err = pk.parseSlhdsa(r, slhdsa.ModeSimpleSHA2) + case PubKeyAlgoSlhdsaShake: + err = pk.parseSlhdsa(r, slhdsa.ModeSimpleShake) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -539,6 +639,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-wussler-openpgp-pqc-03.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) { pk.oid = new(encoding.OID) if _, err = pk.oid.ReadFrom(r); err != nil { @@ -681,6 +816,107 @@ 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 +} + +// parseMldsaEcdsa parses a ML-DSA + ECDSA public key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +func (pk *PublicKey) parseMldsaEcdsa(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_ecdsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + } + + if pub.Curve, err = GetEcdsaCurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if err := pub.UnmarshalPoint(pk.p.Bytes()); err != nil { + return err + } + + pub.PublicMldsa = pub.Mldsa.PublicKeyFromBytes(pk.q.Bytes()) + + pk.PublicKey = pub + + return +} + +// parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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 + } + + pub.PublicMldsa = pub.Mldsa.PublicKeyFromBytes(pk.q.Bytes()) + + pk.PublicKey = pub + return +} + +// parseSlhdsa parses a SLH-DSA public key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 +func (pk *PublicKey) parseSlhdsa(r io.Reader, mode slhdsa.Mode) (err error) { + var id slhdsa.ParameterSetId + pub := new(slhdsa.PublicKey) + + var param [1]byte + if _, err = readFull(r, param[:]); err != nil { + return + } + + if id, err = slhdsa.ParseParameterSetID(param); err != nil { + return + } + + pk.slhDsaParameterSetId = id + pub.ParameterSetId = id + pub.Mode = mode + pub.Parameters, err = slhdsa.GetParametersFromModeAndId(mode, id) + + pk.p = encoding.NewEmptyOctetArray(pub.ParameterSetId.GetPkLen()) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + if err := pub.UnmarshalPublic(pk.p.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub return } @@ -774,6 +1010,15 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: length += 1 // Hash octet length += 32 // Binding hash + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, + PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.q.EncodedLength()) + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + length += 1 // ParamID octet + length += uint32(pk.p.EncodedLength()) default: panic("unknown public key algorithm") } @@ -882,13 +1127,28 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(symmKey.BindingHash[:]) return + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, + PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + if _, err = w.Write(pk.p.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.q.EncodedBytes()) + return + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + if _, err = w.Write(pk.slhDsaParameterSetId.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.p.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 @@ -979,6 +1239,26 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("HMAC verification failure") } return nil + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + mldsaEddsaPublicKey := pk.PublicKey.(*mldsa_eddsa.PublicKey) + if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.EdDSASigR.Bytes()) { + return errors.SignatureError("mldsa_eddsa verification failure") + } + return nil + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, + PubKeyAlgoMldsa87Brainpool384: + mldsaEcdsaPublicKey := pk.PublicKey.(*mldsa_ecdsa.PublicKey) + if !mldsa_ecdsa.Verify(mldsaEcdsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.ECDSASigR.Bytes(), sig.ECDSASigS.Bytes()) { + return errors.SignatureError("mldsa_ecdsa verification failure") + } + return nil + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + spxPublicKey := pk.PublicKey.(*slhdsa.PublicKey) + if sig.slhDsaParameterSetId != spxPublicKey.ParameterSetId || + !slhdsa.Verify(spxPublicKey, hashBytes, sig.SlhdsaSig.Bytes()) { + return errors.SignatureError("SLH-DSA verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1209,6 +1489,13 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed448.PublicKeySize * 8 case ExperimentalPubKeyAlgoAEAD: bitLength = 32 + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, + PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, + PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + bitLength = pk.q.BitLength() // Very questionable + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + bitLength = pk.p.BitLength() // Even more questionable default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1247,3 +1534,113 @@ 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() } + +func GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return PubKeyAlgoMlkem768X25519, nil + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + return PubKeyAlgoMlkem1024X448, nil + case PubKeyAlgoMldsa65p256: + return PubKeyAlgoMlkem768P256, nil + case PubKeyAlgoMldsa87p384: + return PubKeyAlgoMlkem1024P384, nil + case PubKeyAlgoMldsa65Brainpool256: + return PubKeyAlgoMlkem768Brainpool256, nil + case PubKeyAlgoMldsa87Brainpool384: + return PubKeyAlgoMlkem1024Brainpool384, 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: + return mlkem768.Scheme(), nil + case PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: + 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 + case PubKeyAlgoMlkem768P256: + return ecc.NewGenericCurve(elliptic.P256()), nil + case PubKeyAlgoMlkem1024P384: + return ecc.NewGenericCurve(elliptic.P384()), nil + case PubKeyAlgoMlkem768Brainpool256: + return ecc.NewGenericCurve(brainpool.P256r1()), nil + case PubKeyAlgoMlkem1024Brainpool384: + return ecc.NewGenericCurve(brainpool.P384r1()), nil + default: + return nil, goerrors.New("packet: unsupported ECDH public key algorithm") + } +} + +func GetEcdsaCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDSACurve, error) { + switch algId { + case PubKeyAlgoMldsa65p256: + return ecc.NewGenericCurve(elliptic.P256()), nil + case PubKeyAlgoMldsa87p384: + return ecc.NewGenericCurve(elliptic.P384()), nil + case PubKeyAlgoMldsa65Brainpool256: + return ecc.NewGenericCurve(brainpool.P256r1()), nil + case PubKeyAlgoMldsa87Brainpool384: + return ecc.NewGenericCurve(brainpool.P384r1()), nil + default: + return nil, goerrors.New("packet: unsupported ECDSA 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 GetSlhdsaModeFromAlgID(algId PublicKeyAlgorithm) (slhdsa.Mode, error) { + switch algId { + case PubKeyAlgoSlhdsaSha2: + return slhdsa.ModeSimpleSHA2, nil + case PubKeyAlgoSlhdsaShake: + return slhdsa.ModeSimpleShake, nil + default: + return 0, goerrors.New("packet: unsupported EdDSA public key algorithm") + } +} + +func GetAlgIDFromSlhdsaMode(mode slhdsa.Mode) PublicKeyAlgorithm { + switch mode { + case slhdsa.ModeSimpleSHA2: + return PubKeyAlgoSlhdsaSha2 + case slhdsa.ModeSimpleShake: + return PubKeyAlgoSlhdsaShake + default: + panic("invalid SLH-DSA mode") + } +} + +func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (dilithium.Mode, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: + return dilithium.MLDSA65, nil + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: + return dilithium.MLDSA87, 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 c32d7ad2..7f154d9a 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -14,6 +14,11 @@ import ( "strconv" "time" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/cloudflare/circl/sign/dilithium" + "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" "github.com/ProtonMail/go-crypto/openpgp/ed448" @@ -64,8 +69,13 @@ 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 + + // slhDsaParameterSetId contains the parameter set ID for the SLH-DSA instantiation + slhDsaParameterSetId slhdsa.ParameterSetId // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -174,7 +184,10 @@ 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, PubKeyAlgoMldsa65p256, + PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384, + PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -317,12 +330,80 @@ 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, dilithium.MLDSA65.SignatureSize()); err != nil { + return + } + case PubKeyAlgoMldsa87Ed448: + if err = sig.parseMldsaEddsaSignature(r, 114, dilithium.MLDSA87.SignatureSize()); err != nil { + return + } + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: + if err = sig.parseMldsaEcdsaSignature(r, 32, dilithium.MLDSA65.SignatureSize()); err != nil { + return + } + case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: + if err = sig.parseMldsaEcdsaSignature(r, 48, dilithium.MLDSA87.SignatureSize()); err != nil { + return + } + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + if err = sig.parseSlhdsaSignature(r); err != nil { + return + } default: panic("unreachable") } return } +// parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.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 +} + +// parseMldsaEcdsaSignature parses a ML-DSA + ECDSA signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-packet-tag-2 +func (sig *Signature) parseMldsaEcdsaSignature(r io.Reader, ecLen, dLen int) (err error) { + sig.ECDSASigR = encoding.NewEmptyOctetArray(ecLen) + if _, err = sig.ECDSASigR.ReadFrom(r); err != nil { + return + } + + sig.ECDSASigS = encoding.NewEmptyOctetArray(ecLen) + if _, err = sig.ECDSASigS.ReadFrom(r); err != nil { + return + } + + sig.MldsaSig = encoding.NewEmptyOctetArray(dLen) + _, err = sig.MldsaSig.ReadFrom(r) + return +} + +// parseSlhdsaSignature parses a SLH-DSA signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-packet-tag-2-2 +func (sig *Signature) parseSlhdsaSignature(r io.Reader) (err error) { + var param [1]byte + if _, err = readFull(r, param[:]); err != nil { + return + } + + if sig.slhDsaParameterSetId, err = slhdsa.ParseParameterSetID(param); err != nil { + return + } + + sig.SlhdsaSig = encoding.NewEmptyOctetArray(sig.slhDsaParameterSetId.GetSigLen()) + _, err = sig.SlhdsaSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 4880, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -968,6 +1049,41 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e if err == nil { sig.HMAC = encoding.NewShortByteString(sigdata) } + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, + PubKeyAlgoMldsa87Brainpool384: + if sig.Version != 6 { + return errors.UnsupportedError("cannot use mldsa_ecdsa on a non-v6 signature") + } + sk := priv.PrivateKey.(*mldsa_ecdsa.PrivateKey) + dSig, ecR, ecS, err := mldsa_ecdsa.Sign(config.Random(), sk, digest) + + if err == nil { + sig.MldsaSig = encoding.NewOctetArray(dSig) + sig.ECDSASigR = encoding.NewOctetArray(ecR) + sig.ECDSASigS = encoding.NewOctetArray(ecS) + } + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if sig.Version != 6 { + return errors.UnsupportedError("cannot use mldsa_eddsa on a non-v6 signature") + } + sk := priv.PrivateKey.(*mldsa_eddsa.PrivateKey) + dSig, ecSig, err := mldsa_eddsa.Sign(sk, digest) + + if err == nil { + sig.MldsaSig = encoding.NewOctetArray(dSig) + sig.EdDSASigR = encoding.NewOctetArray(ecSig) + } + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + if sig.Version != 6 { + return errors.UnsupportedError("cannot use SLH-DSA on a non-v6 signature") + } + sk := priv.PrivateKey.(*slhdsa.PrivateKey) + spxSig, err := slhdsa.Sign(sk, digest) + + if err == nil { + sig.slhDsaParameterSetId = sk.ParameterSetId + sig.SlhdsaSig = encoding.NewOctetArray(spxSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1073,7 +1189,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") } @@ -1096,6 +1212,17 @@ 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()) + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, + PubKeyAlgoMldsa87Brainpool384: + sigLength = int(sig.ECDSASigR.EncodedLength()) + sigLength += int(sig.ECDSASigS.EncodedLength()) + sigLength += int(sig.MldsaSig.EncodedLength()) + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + sigLength = 1 // Parameter ID + sigLength += int(sig.SlhdsaSig.EncodedLength()) default: panic("impossible") } @@ -1204,6 +1331,25 @@ 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()) + case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, + PubKeyAlgoMldsa87Brainpool384: + if _, err = w.Write(sig.ECDSASigR.EncodedBytes()); err != nil { + return + } + if _, err = w.Write(sig.ECDSASigS.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.MldsaSig.EncodedBytes()) + case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + if _, err = w.Write(sig.slhDsaParameterSetId.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.SlhdsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/pqc_vectors_test.go b/openpgp/pqc_vectors_test.go new file mode 100644 index 00000000..5c242076 --- /dev/null +++ b/openpgp/pqc_vectors_test.go @@ -0,0 +1,217 @@ +// 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. + +//go:build pqc_test_vectors + +package openpgp + +import ( + "bytes" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" + "strings" + "testing" +) + +func dumpTestVector(t *testing.T, filename, vector string) { + t.Logf("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, 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 TestV4EddsaPqKey(t *testing.T) { + //eddsaConfig := &packet.Config{ + // DefaultHash: crypto.SHA512, + // Algorithm: packet.PubKeyAlgoEdDSA, + // V6Keys: false, + // 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: false, + // 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) + //} + + entities, err := ReadArmoredKeyRing(strings.NewReader(v4Ed25519Mlkem768X25519PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + + entity := entities[0] + + serializePqSkVector(t, "v4-eddsa-sample-pk.asc", entity, true) + serializePqPkVector(t, "v4-eddsa-sample-pk.asc", entity, true) + + t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) + } + + var configV1 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: nil, + } + + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1,true) + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2,false) +} + + +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 + // }, + //} + // + //entity.Subkeys = []Subkey{} + //err = entity.AddEncryptionSubkey(kyberConfig) + //if err != nil { + // t.Fatal(err) + //} + + entities, err := ReadArmoredKeyRing(strings.NewReader(v6Ed25519Mlkem768X25519PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + + entity := entities[0] + + serializePqSkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + + t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2,false) +} diff --git a/openpgp/read.go b/openpgp/read.go index 43def2c4..0a43c589 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -23,6 +23,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -118,7 +121,10 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, packet.PubKeyAlgoMlkem1024P384, + packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: break default: continue diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 99c390bd..bbc6c7cc 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -12,6 +12,7 @@ import ( "io" "io/ioutil" "os" + "strconv" "strings" "testing" @@ -939,3 +940,101 @@ 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 + v6 bool +}{ + "v4_Ed25519_ML-KEM-768+X25519": { + v4Ed25519Mlkem768X25519PrivateTestVector, + v4Ed25519Mlkem768X25519PublicTestVector, + []string{"b2e9b532d55bd6287ec79e17c62adc0ddd1edd73", "95bed3c63f295e7b980b6a2b93b3233faf28c9d2", "bd67d98388813e88bf3490f3e440cfbaffd6f357"}, + []string{v4Ed25519Mlkem768X25519PrivateV1MessageTestVector, v4Ed25519Mlkem768X25519PrivateV2MessageTestVector}, + false, + + }, + "v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + true, + }, +} + +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, !test.v6) + 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 77282c0e..f2a298c8 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -473,3 +473,377 @@ jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD =8TxH -----END PGP PRIVATE KEY BLOCK----- ` + +// PQC keys and messages +const v4Ed25519Mlkem768X25519PrivateHex = "c5580451d0c68016092b06010401da470f010107408db47ca20d568541a5af642c5732c9d48b1f6d06099be582763b7982d5bb82580000fe2d90e8f21e63d3e96dd8e816e79e07d526e4939b84bda07412d3f24e3c009b2c122bcd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec28f0413160a0041050251d0c680091077ca82cda8eec0a6162104f9a0bc4d86c90113272d809277ca82cda8eec0a6021b03021e01021901030b090703150a0802160005270902070200007bef0100dc8e6db6e888059aee02d2ac09e813feeb5726fa1f4687b59a3d17667ab02f6400ff44a946bad6674abd35f621f5dfee830d808bdb3f0c5be33f5a156a9f74d9770fc7cd890451d0c6806953228a1963256c93e9f803bbe6785b5b3d0015f38cab3411368a8b30dadf0420ae0419683bbf5a9307e2582bc7ec3efc536eda5c84c3916eb0e8836389846050126322388079abd78bc557e79c8ba3c847a668fae94b42dc8fac584bf1226921eb3a06c18979837e565428200876cad01918a01b27b18ced8c231ad724acc069f1d445f9e3ad139a51b333090967a6c500688fa9cad1908ef6208805e131d92176ac9383ac779a37049e8f9b9227f73e9f26a99c34481a49b3f8a938a49496d2a706ebfba9c8077dfad75950a59d25401241468ab57c4579124635b7b626495232c72473fb5a6635339e6b4b23fb0457f77a6a7c8f7431016de951a3772104388d183b3687b65c5f01833c485f4fe966de10a4f2f71569f431b255b5263691bf45b5a89186efc62ea33c6c05f63afb69a3787a87e029c919d664d5351e96c191df70667c04920e7b1934e58552745c4014cb6ea15d4afc98d046ada704223c3688db549524c1844250030dc83524f5608cb412f0394614aa45a0696af6c8ccf1e1c57718cc6ef138f8b36c37701f06f7b12df13be432905f88c55a799415acc4ae97c33e8357d8d7bdd657551047c3a132a1b0a8c323e35359fbce2bdb1306262913ea7a653c93b897bfbf2485a2c9564b096a6c636296730e1fc888e8362a4efb5670a85024769ca7d6380732abac2b6b3f85ad2a489800f5add0546f2e85602d01cb259b3524b630e362a4aebb41137751dbf5ab0971ce9788b0065458d6e2194de08bf2ec778687902bb572e2e207d4f77f3c824d532c9a31a0a440a2cbb4306c581956ddc7b3e15b019c5187e66a95879c5917432afc139c3858938222a9ec79c64c7579fbf29e55676616989dd36441b1b25b489b66332ca2c9a71dc8850f0c73a20e46096cb4924f24129661509348465b864fe03362960099b00071aef68da14a2eedb735533abfd6d39582663669b962f7266cfd711bd795a15f24b515172a4db788ab3805875a054380608ae204c731a9f10bbd2cf9c735a344912a8fd6f69738b92a257370b1fb3f3ed03c6495b588578b77a29f9d1b23d212615db28b06e004357491b7b8403f54c36bc302a4f998cd08336714b21ca8a98dd7beba6978ba987fb254c5c22b3004d2b2c45b6b7b103aef86b767135e274acc6676a10cfc419344c5053a3ec32740e35c1a597c13f8e74396b3433da42b67a70afd88180c656cbf2763445c34852cb558386f4757a44175000e30bda7773b92fba43090631a6c5ed4f4b51732ad7b864963d02924e06ac2968e2dcbb75322c1fef3c4a307682c16a908d3708592808083c61e604888b53264e44445385dfa6219bcd7c5a5ecb332e363bc2a3bbf719187333bd0b47b04a7a9a308877e31bd1754ba9c990f04f08f61db18b3063bf6d2aaa58a6f431b6921a5adf1b59b1c51a667d21401e8aea11a79a1a521df79ceb7c2596b72c6bfba49da808ff0a9aafec1736f42bc99b2b950e38e50f3ae6c476d4e9032c556237b836cb26263dc0815d0cb3b546733924063a606499b585c0b891d0b817febd9cedf82c26c84abb94a146dec7fc1054716c33409f995bd948ea30a1ec0d77745e89c9277c949b40d0e7b60287026d710a56762966db08b00cc704865ab845c28f6fc7806e675549366a0842b4a43ba9ac93d47e160daa84215549d06f2e20945b5f73d603c13c5ad452390d200ab70af27a845dea5fed96fda6ad403c05fe39764b7e530bc016d40e695a756d388e49e020633b5c876d53b5d28072099803898caa10cf6249dab4031c8a4a3fc61c187791f39ab4e1957adb2bef4aabaa90c8128b1235605892f081f2e658d24535a6c7701ce9a1ea79a20b923563b583d10d9900280ade4c3c436d297066c0e29710f56cb2e32c3113c78ce1c12165a6626ea5c8a01eaab7c0c4e80eb5a342b8d2fd6561f907722909d4cd3b78c1bb6552a63516b5912e5c9cdc75a18699cbcb181f3f37d5865bdf6c04671132b48ca5e98b232ac410bf5926c212cb5258047f6158fded0c881f12995852b2f1cbeef7c0d1c22bd07a80bdec417cc462f0c36467c1caa3af015f8c1753a5a5116406adc33b52657b1fcd87bfb7c7453d51b01db8b68e83c08355235001a78a12cdfc1495b3a26e7518eac828699ea54c9a09b92e4069a3074bcdb0c4c6b7a40d62e6592598984cf4b295265a7370b0b1fb5038efc0302bcc19548e36137410e86622ef0a22b31741f07b9553ad54a907466799a4fe027c2f5b54a0e98a89f2aab17807f4106282279bea94088e7110abf03cbdcd25e3c39694779641903a5efb4bd17b42405e07faaa4728f4a6a161b4e015885f9c17c7182546a3272f60434060a7b59266223f046fd600d79a843f85221a1779ff5807c96b378625c80ef164ad0da095e762ef2b30bc2e195e377832f838f397501e2fc3169ca56a34532c3834949558ddb750023078ec2c85202d292f9b109c1b866cd91c1f7443f2c097d90760f83548507d1cf99a649443a6b517c91933b288c33731b406e8c91cee4852dd4dc1dbe26c9df744f663152e9963daf491ec7073237fc05c44194f1ac3408e430694c4864a96a59bb6d9dd39d02e9cd0bab6754f0252930a40cb73d0a184cb738879d9bc14371797d7b5fa4413c2be555e68c19cb9c84dfd07354d768da8104fff3283686ac8e0780102c5809b033863c211f14c9d2a02452cc7b3ce85fb958b9512c1054b7a341fa471d3104e228c8b2e348685527e545bf1c4448b0829daff4ac2347a883953c1c9ca9830b841382b88a066c4b516d68db184bb473eff1bf970cc2e884a24cd1055f2acd16692f7910917301701741c4bea0466ac31260c22c58415f238cc681848329e0a0d963497b67ca52e3a52352aea5fc8c77e24ee4fa2a46f3a724f1be044a6adc5634d8129c96c5523abc113fac195c837b1067bc21e77a9e68a713a316600a58cc8570dab4cdc540cc414a56d194a36df8c660ca4153a7b329504237aa36a546319a12ae4004aa6d138c6ca51f5c4b5dabcc1be1cb2e8c9120f4112548b8823f2b5aa0026fc05404e77a701295b3e40aad93743fcd791639b06b33e194360b7c3bd82e917c9613fc784662a49a4a270c9b63f9b5161e2a525647b522172ccc627420c97520d11f130561f4a4be9d977a3d32a401e72a51647249b9924ad81ede73473de38e31c8a7ad8aa1e00676df52788f5133d9865ba632a0ed16b0d7596bf659c798bb52a0835f2330446321951639580d319cdcda1ced19ae6b737e6d012374541a44579507515a3182b8852721186197db61bb9686a25383c8e8965a3c888e30bc456fb92dcbb2390bfa1023d6ce1dc261bd1cc79177683d261ee1e9ca182cc80f87b5ae0419683bbf5a9307e2582bc7ec3efc536eda5c84c3916eb0e8836389846050126322388079abd78bc557e79c8ba3c847a668fae94b42dc8fac584bf1226921eb3a06c18979837e565428200876cad01918a01b27b18ced8c231ad724acc069f1d445f9e3ad139a51b333090967a6c500688fa9cad1908ef6208805e131d92176ac9383ac779a37049e8f9b9227f73e9f26a99c34481a49b3f8a938a49496d2a706ebfba9c8077dfad75950a59d25401241468ab57c4579124635b7b626495232c72473fb5a6635339e6b4b23fb0457f77a6a7c8f7431016de951a3772104388d183b3687b65c5f01833c485f4fe966de10a4f2f71569f431b255b5263691bf45b5a89186efc62ea33c6c05f63afb69a3787a87e029c919d664d5351e96c191df70667c04920e7b1934e58552745c4014cb6ea15d4afc98d046ada704223c3688db549524c1844250030dc83524f5608cb412f0394614aa45a0696af6c8ccf1e1c57718cc6ef138f8b36c37701f06f7b12df13be432905f88c55a799415acc4ae97c33e8357d8d7bdd657551047c3a132a1b0a8c323e35359fbce2bdb1306262913ea7a653c93b897bfbf2485a2c9564b096a6c636296730e1fc888e8362a4efb5670a85024769ca7d6380732abac2b6b3f85ad2a489800f5add0546f2e85602d01cb259b3524b630e362a4aebb41137751dbf5ab0971ce9788b0065458d6e2194de08bf2ec778687902bb572e2e207d4f77f3c824d532c9a31a0a440a2cbb4306c581956ddc7b3e15b019c5187e66a95879c5917432afc139c3858938222a9ec79c64c7579fbf29e55676616989dd36441b1b25b489b66332ca2c9a71dc8850f0c73a20e46096cb4924f24129661509348465b864fe03362960099b00071aef68da14a2eedb735533abfd6d39582663669b962f7266cfd711bd795a15f24b515172a4db788ab3805875a054380608ae204c731a9f10bbd2cf9c735a344912a8fd6f69738b92a257370b1fb3f3ed03c6495b588578b77a29f9d1b23d212615db28b06e004357491b7b8403f54c36bc302a4f998cd08336714b21ca8a98dd7beba6978ba987fb254c5c22b3004d2b2c45b6b7b103aef86b767135e274acc6676a10cfc419344c5053a3ec32740e35c1a597c13f8e74396b3433da42b67a70afd88180c656cbf2763445c34852cb558386f4757a44175000e30bda7773b92fba43090631a6c5ed4f4b51732ad7b864963d02924e06ac2968e2dcbb75322c1fef3c4a307682c16a908d3708592808083c61e604888b53264e44445385dfa6219bcd7c5a5ecb332e363bc2a3bbf719187333bd0b47b04a7a9a308877e31bd1754ba9c990f04f08f61db18b3063bf6d2aaa58a6f431b6921a5adf1b59b1c51a667d21401e8aea11a79a1a521df79ceb7c2596b72c6bfba49da808ff0a9aafec1736f42bc99b2b950e38e50f3ae6c476d4e9032c556237b836cb26263dc0815d0cb3b546733924063a606499b585c0b891d0b817febd9cedf82c26c84abb94a146dec7fc1054716c33409f995bd948ea30a1ec0d77745e89c9277c949b40d0e7b60287026d710a56762966db08b00cc704865ab845c28f6fc7806e675549366a0842b4a43ba9ac93d47e160daa84215549d06f2e20945b5f73d603c13c5ad452390d2b28c44046b6117559a75c255de0944a4699b34fd2bd4859fb261d9eb7e445823c6ad5f7a19ba7c36da8d10918aad8373d3e43ab24a020980736fda04cbdfe98d6f5ac2780418160a002a050251d0c680091077ca82cda8eec0a6162104f9a0bc4d86c90113272d809277ca82cda8eec0a6021b0c000051b80100f05de3a23334bebca47402a4e7cb23900ab5c365cf4b0fb7cd1381690763f5010100aaa0984cb139e3c1c3a51aa577ec32b4d42e44bfc8f9cfff5dc8459cc05e8d0b" + +const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v4Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +0O22blDNAr/tDA== +=q5En +-----END PGP PRIVATE KEY BLOCK-----` + +const v4Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=dPFW +-----END PGP PUBLIC KEY BLOCK-----` + +const v4Ed25519Mlkem768X25519PrivateV1MessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPUA+RAz7r/1vNXaUNGH8CAkSiFgunnUDqAiD9JSd3Sb7lMNUsWk6lzWiJicgky +S/vu0sSnRtxweWkoMr1y2ZaS45nXbEQyShiqHhZUKfVwtxbU+rGVH5oCgSvtTCrs +verZaFpqzqPWyZ8ApzJvjbGUDBuwns09dGIKvKoePT5DCrqXlsW4EA8gFJbiXeb3 +E7nsyg3l2uMzbt6FHtYoa6qq9Q0PsUiGte52nXXWEnmBOGUfmCkVsgmHDmz63BLT +1xXuZ5YopZkhhpjTNtvWtXc6MIaqnh6XtAcg8ZoaH0iferpbHEp9+M4bv5YDjzji +vv83rBQN4cBaS1/TSmBkNJHmxcyT1AOOXY2ZbmxQBORhGOTrFz3w8R78MYkEvB6x +JAjoYirpsyNLJzdewpXEYrPQq4Ey8EG2+qDY47vQkQaYcSFFoxYQ8MpHXmmgJ2bp +D13g/lQlSHcdWX2L59Wa1dhKRVnUyeEtO5c06FKJ7QOrywNjPdVciPVCx6bBfVd2 +6qiWLynSGnzGaKd1YyaviioCm48Ydu5q8Z+QbEANbKW1azVAWCuxuiomE3RBvf1O +8d30UvBnImEf+9ANDxzmjIG2lW39U591Jbv0pL00at3tIMQN2wwiduP1KZ1dilWa +gEkdPjl6Q68ov0vRCYMAZizj4pMZbsUdge2Jj9GieObnp+w25pJu9nBeI6iqYmwd +Ny1U3OuvzbEUsNfKcHoQd9Cem8EZn+5ICk7eqsTkZq69oYfIVRyzEEc/X9562nzh +6B+X4CHZY/C8UCWougQriG4KVszM4myOgekKg0kNVIWgE2y7Z//S9c2twdxRWT/a +8QC4p7QX7JRgzDD9erkj/9J3hKwHxDHShKB5jsVaGO+BxtFSCiiTmgeo7+SAnJwU +Mi/N0UiI2BbKdo4KmdDPUVDyobBjCjeXil7Kg7pTU0vewPZQDLl9X16CcXCB60HL +fkDGpcYbjkZYbmB449sQfaLvxRMHomP4TY4PEfANIXdWmk1mS0/+zNzMQ9+Xderc +8P/EdKDKF5yr7IzSNoxuLiIWpyWJj+5QmAwup9mVv5gkh5RPnUQ0fgQ1vU8K9PMz +OmYqlX2W4gPn29UovjkbGH+lEzazEzA7VZWHXG86NVN8WMXqdQvMJcmMRZhDmC3F +kCII5zc6dxFXjNUgaAqV8eBqvRBbgCqK+6HSwCMY7jNFhFIy+Nj/9BYU/ereax0t +Zlsk7XDK9lMZUidh5+VeEqbyMsLQ0YiyO7VJ5VdiPESXHjPkzxo42XZJELuBVC9D +ArAX2Qip+oV1RXzhu/SeJdRQufGSENeZpGiG4tW24dpROh40I5TgXmpd4ALhuh1S +PrepCNhXuFtKDIStKZEmCknPAGWAkLYZz5rAaMtztdGvzlektn+8CDtSo3d6FUww +dp68ZtSMMb5HGscAoiDoOTiB5KVPSd80s3EPXlsgQSfHuSUHTvmD8G6q4hqGXMeV +IUdwjwTvDMfW7CU5zqiV01SO6dXKsFyjLJrT57kpCbQ/2fhoMC+kNcXpzI+Z65yI +jCP6Sjv+cVh7tv55kTKAPHO5VE3MDxvSOQHpUQ0zora+lfzpLUahfv8uZ4Q4J3L6 +mkHfXuplyv3LcunejQDog2bhakqbrb5lg3fZGYNagykZxw== +=2Xhi +-----END PGP MESSAGE-----` + +const v4Ed25519Mlkem768X25519PrivateV2MessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPhBhUEvWfZg4iBPoi/NJDz5EDPuv/W81dpZ1Yz1yu1Dk/HK2JuEmE6RavqzhvT +i508AZhPxC08BxfNFar+uyZCNyMrUSrY0qY8H61GTtx1+O9VynXl8uXtS1nTDGJ9 +vCR+EvH6rT/gOPQB8HUhX6Ps97Yqi/Iys1gfS8n961pScwIYpPJzUWfUUKjIT55W +htkh9aIB6unqzwUDi3p4oRZRm67j1ZP14SLyonAG2tXtCZyu1An62UHeOyNl1/6Z +CgC3egTf6lz26US15T8AP54AO77LOf9KwLpUYcwvSExqHGgmhS0Mil6WnFyuJUDB +7A2T2p/koW7TDaqoxhWsxY2isiH1SmAxNxzMnrGd7rNpPJ/k/r42bILfOuG0TRUN +zqC9ph6OdydSyhHkN5G4eOYQqqvk19/lfLuHWlNwfNcn/2PsgsxLxNj7ltVn90W0 +qLubPWrujn/DhLl+hs2xXDOudpcztUqxcBnrsSaHlaebjQoDfttVAQj2jjdNXRjZ +uNRnRfcG9s3sO3b8d4ed6tk6U+nMrE2dZCBjTagqvD07Z1TpZDh7t86V3X16o/ps +jxW42s+YR589b88IZcieZRbKVtXt00pn2tn95kpvL3d8nAkaiPUhrowQUz0jpn8c +CDBNAn1j690qM3pD5XJlwverC2cmJH1Hjobnrhi6X1k2lQxweX28p+R9NQjSoX0h +ORuE0/Wpi15y0xmr2EzjcZ/6vPncy/IrYJCYmx9+aWQAjrKjizzNFTt73kf1xba5 +t4tbZkj9xgdDJXq3bAqB0/JeeTb4aTCk+n4olVYzCnMtLgj+1fWPClMModACmFOG +1+bw5Q91/7euo363sw5UwgU1JhSQ/xcKNyJQsnklWkLMJNB1Yhj/C32lEmLntigv +UOO510+ehA7D5ftef8cMfEIm73HrBBiLfixvVTR8AQV4hiV/mzKP7weM7kxvAvbz +ir4jt3uSBOuhTjzq2is/S3D2K+O8FZqGIbkDhnKd98LbEA2cn9nTfsbV+TVXCmaS +lHNojVxPL2pUKxedV5skvfflRFciuP7UNsf8myHe7wdfPdSzMsbytDEwID3vcsme +fBqZdEZxqv/mNnn38TfHMSCF+yv5XbF9ham4DIcqNlkYud1ipEFFbcBZ0o9nUIWp +diSY7KGAtVF224dtcr3FTHGuBnayDq+Yk++VhF4Bb3uPVuwrkf7Bncp1aYEQfkhI +HwF3X6GnwC3y7kpbkU1rOq7yXv/0mRyGpVQlW/Yf3qT1buxcWt5BvXBmKzbBpVg/ +0B9vpzrlFsT0Pb2GHuQ6U+9JoZ+ePnRMVdDz93RCGr1kQlyY15K1b+yILJiV6oOL +OxoxXHnr5soIumxCqv+6oAm4SdQVJLELQK72x1dVKJ90jUOgYCeOY61NsC9BFWHT +h0itUEnwWMjKg73z00bthndwfEXHBJLrHizkcv+pwD8M5wb/9H6HU4x8ELSr5Fyn +WjSoa2739wmJkoJY5ifaic3L8UXJeLuEZnVG9tUrl9ohHO8RNR3Vc/uHmyhImoYp +RL4rcc6YpuyextmYu9S9LkPR5Bzr+mFeJDeXbA7GJm9eofdw0lQCCQIMAGc2j84/ +tfivyP5YrgQ8uBt9iwJN3IYRBy8qdr9JUyxkpkOEshV6XE4g3Orpbx0ZdrxbKmDS +7eJl5fSust3gb2KfaAoWkFQivVJP2KTl5gw= +-----END PGP MESSAGE-----` + +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/slhdsa/parameter.go b/openpgp/slhdsa/parameter.go new file mode 100644 index 00000000..67d63a72 --- /dev/null +++ b/openpgp/slhdsa/parameter.go @@ -0,0 +1,85 @@ +// Package slhdsa implements SLH-DSA suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-2 +package slhdsa + +import ( + goerrors "errors" +) + +// ParameterSetId represents the security level parameters defined in: +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-parameters-and-arti +type ParameterSetId uint8 +const ( + Param128s ParameterSetId = 1 + Param128f ParameterSetId = 2 + Param192s ParameterSetId = 3 + Param192f ParameterSetId = 4 + Param256s ParameterSetId = 5 + Param256f ParameterSetId = 6 +) + +// ParseParameterSetID parses the ParameterSetId from a byte, returning an error if it's not recognised +func ParseParameterSetID(data [1]byte) (setId ParameterSetId, err error) { + setId = ParameterSetId(data[0]) + switch setId { + case Param128s, Param128f, Param192s, Param192f, Param256s, Param256f: + return setId, nil + default: + return 0, goerrors.New("packet: unsupported SLH-DSA parameter id") + } +} + +// GetPkLen returns the size of the public key in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms +func (setId ParameterSetId) GetPkLen() int { + switch setId { + case Param128s, Param128f: + return 32 + case Param192s, Param192f: + return 48 + case Param256s, Param256f: + return 64 + default: + panic("slhdsa: unsupported parameter") + } +} + +// GetSkLen returns the size of the secret key in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms +func (setId ParameterSetId) GetSkLen() int { + switch setId { + case Param128s, Param128f: + return 64 + case Param192s, Param192f: + return 96 + case Param256s, Param256f: + return 128 + default: + panic("slhdsa: unsupported parameter") + } +} + +// GetSigLen returns the size of the signature in octets according to +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms +func (setId ParameterSetId) GetSigLen() int { + switch setId { + case Param128s: + return 7856 + case Param128f: + return 17088 + case Param192s: + return 16224 + case Param192f: + return 35664 + case Param256s: + return 29792 + case Param256f: + return 49856 + default: + panic("slhdsa: unsupported parameter") + } +} + +func (setId ParameterSetId) EncodedBytes() []byte { + return []byte{byte(setId)} +} diff --git a/openpgp/slhdsa/sphincs.go b/openpgp/slhdsa/sphincs.go new file mode 100644 index 00000000..bf9a8be7 --- /dev/null +++ b/openpgp/slhdsa/sphincs.go @@ -0,0 +1,155 @@ +// Package slhdsa implements SLH-DSA suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-2 +package slhdsa + +import ( + "crypto/subtle" + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/kasperdi/SPHINCSPLUS-golang/parameters" + "github.com/kasperdi/SPHINCSPLUS-golang/sphincs" +) + +// Mode defines the underlying hash and mode depending on the algorithm ID as specified here: +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-parameter-specification +type Mode uint8 +const ( + ModeSimpleSHA2 Mode = 1 + ModeSimpleShake Mode = 2 +) + +type PublicKey struct { + ParameterSetId ParameterSetId + Mode Mode + Parameters *parameters.Parameters + PublicData *sphincs.SPHINCS_PK +} + +type PrivateKey struct { + PublicKey + SecretData *sphincs.SPHINCS_SK +} + +func (priv *PrivateKey) SerializePrivate ()([]byte, error) { + return priv.SecretData.SerializeSK() +} + +func (priv *PrivateKey) UnmarshalPrivate (data []byte) (err error) { + // Copy data to prevent library from using an older reference + serialized := make([]byte, len(data)) + copy(serialized, data) + + priv.SecretData, err = sphincs.DeserializeSK(priv.Parameters, serialized) + if err != nil { + return err + } + + return nil +} + +func (pub *PublicKey) SerializePublic ()([]byte, error) { + return pub.PublicData.SerializePK() +} + +func (pub *PublicKey) UnmarshalPublic (data []byte) (err error) { + // Copy data to prevent library from using an older reference + serialized := make([]byte, len(data)) + copy(serialized, data) + + pub.PublicData, err = sphincs.DeserializePK(pub.Parameters, serialized) + if err != nil { + return err + } + + return nil +} + +// GenerateKey generates a SLH-DSA key as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation +func GenerateKey(_ io.Reader, mode Mode, param ParameterSetId) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.ParameterSetId = param + priv.Mode = mode + if priv.Parameters, err = GetParametersFromModeAndId(mode, param); err != nil { + return nil, err + } + + // TODO: add error handling to library + // TODO: accept external randomness source + priv.SecretData, priv.PublicData = sphincs.Spx_keygen(priv.Parameters) + + return +} + +// Sign generates a SLH-DSA signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-generation-2 +func Sign(priv *PrivateKey, message []byte) ([]byte, error) { + sig := sphincs.Spx_sign(priv.Parameters, message, priv.SecretData) + return sig.SerializeSignature() +} + +// Verify verifies a SLH-DSA signature as specified in +// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-verification-2 +func Verify(pub *PublicKey, message, sig []byte) bool { + deserializedSig, err := sphincs.DeserializeSignature(pub.Parameters, sig) + if err != nil { + return false + } + + return sphincs.Spx_verify(pub.Parameters, message, deserializedSig, pub.PublicData) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if subtle.ConstantTimeCompare(priv.PublicData.PKseed, priv.SecretData.PKseed) == 0 || + subtle.ConstantTimeCompare(priv.PublicData.PKroot, priv.SecretData.PKroot) == 0 { + return errors.KeyInvalidError("slhdsa: invalid public key") + } + + return +} + +// GetParametersFromModeAndId returns the instance Parameters given a Mode and a ParameterSetID +func GetParametersFromModeAndId(mode Mode, param ParameterSetId) (*parameters.Parameters, error) { + switch mode { + case ModeSimpleSHA2: + switch param { + case Param128s: + return parameters.MakeSphincsPlusSHA256128sSimple(false), nil + case Param128f: + return parameters.MakeSphincsPlusSHA256128fSimple(false), nil + case Param192s: + return parameters.MakeSphincsPlusSHA256192sSimple(false), nil + case Param192f: + return parameters.MakeSphincsPlusSHA256192fSimple(false), nil + case Param256s: + return parameters.MakeSphincsPlusSHA256256sSimple(false), nil + case Param256f: + return parameters.MakeSphincsPlusSHA256256fSimple(false), nil + default: + return nil, goerrors.New("slhdsa: invalid sha2 parameter") + } + case ModeSimpleShake: + switch param { + case Param128s: + return parameters.MakeSphincsPlusSHAKE256128sSimple(false), nil + case Param128f: + return parameters.MakeSphincsPlusSHAKE256128fSimple(false), nil + case Param192s: + return parameters.MakeSphincsPlusSHAKE256192sSimple(false), nil + case Param192f: + return parameters.MakeSphincsPlusSHAKE256192fSimple(false), nil + case Param256s: + return parameters.MakeSphincsPlusSHAKE256256sSimple(false), nil + case Param256f: + return parameters.MakeSphincsPlusSHAKE256256fSimple(false), nil + default: + return nil, goerrors.New("slhdsa: invalid shake parameter") + } + default: + return nil, goerrors.New("slhdsa: invalid hash algorithm") + } +} diff --git a/openpgp/slhdsa/sphincs_test.go b/openpgp/slhdsa/sphincs_test.go new file mode 100644 index 00000000..e02b3ff0 --- /dev/null +++ b/openpgp/slhdsa/sphincs_test.go @@ -0,0 +1,124 @@ +// Package slh_dsa_test tests the implementation of SLH-DSA signatures, suitable for OpenPGP, experimental. +package slhdsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string] slhdsa.Mode{ + "SHA2-Simple": slhdsa.ModeSimpleSHA2, + "SHAKE-Simple": slhdsa.ModeSimpleShake, + } + + params := map[string] slhdsa.ParameterSetId { + "1": slhdsa.Param128s, + "2": slhdsa.Param128f, + "3": slhdsa.Param192s, + "4": slhdsa.Param192f, + "5": slhdsa.Param256s, + "6": slhdsa.Param256f, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + for paramName, param := range params { + t.Run(paramName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo, param) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo, param) + }) + } + }) + } +} + +func testvalidateAlgo(t *testing.T, mode slhdsa.Mode, param slhdsa.ParameterSetId) { + key := testGenerateKeyAlgo(t, mode, param) + if err := slhdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Serialize + pkBin, err := key.SerializePublic() + if err != nil { + t.Fatalf("unable to serialize public key") + } + + skBin, err := key.SerializePrivate() + if err != nil { + t.Fatalf("unable to serialize private key") + } + + // Deserialize + if err = key.UnmarshalPublic(pkBin); err != nil { + t.Fatalf("unable to deserialize public key") + } + + if err = key.UnmarshalPrivate(skBin); err != nil { + t.Fatalf("unable to deserialize private key") + } + + if err := slhdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Corrupt the root of the public key + key.PublicData.PKroot[1] ^= 1 + + if err := slhdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid root in key") + } + + // Re-load the correct public key + if err = key.UnmarshalPublic(pkBin); err != nil { + t.Fatalf("unable to deserialize public key") + } + + if err = key.UnmarshalPrivate(skBin); err != nil { + t.Fatalf("unable to deserialize private key") + } + + if err := slhdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + // Corrupt the seed of the public key + key.PublicData.PKseed[1] ^= 1 + + if err := slhdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid seed in key") + } +} + +func testGenerateKeyAlgo(t *testing.T, mode slhdsa.Mode, param slhdsa.ParameterSetId) *slhdsa.PrivateKey { + priv, err := slhdsa.GenerateKey(rand.Reader, mode, param) + 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) + } + + sig, err := slhdsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := slhdsa.Verify(&priv.PublicKey, digest, sig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 5146607c..09ddc253 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -643,6 +643,7 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { + // Todo: check PQC and use AES-256 // 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)}} } diff --git a/openpgp/write_test.go b/openpgp/write_test.go index 315e7323..1ac9e131 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -522,148 +522,241 @@ 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, }, + "v4_Ed25519_ML-KEM-768+X25519": { + v4Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v4_Ed25519_ML-KEM-768+X25519_signed": { + v4Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "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, + }, + //{ + // mldsa87Ed448Mlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87Ed448Mlkem1024X448PrivateHex, + // true, + // true, + //}, + //{ + // mldsa65P256Mlkem768P245PrivateHex, + // false, + // true, + //}, + //{ + // mldsa65P256Mlkem768P245PrivateHex, + // true, + // true, + //}, + //{ + // mldsa87P384_Mlkem1024P384PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87P384_Mlkem1024P384PrivateHex, + // true, + // true, + //}, + //{ + // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, + // false, + // true, + //}, + //{ + // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, + // true, + // true, + //}, + //{ + // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, + // true, + // true, + //}, + //{ + // slhDsaSha2Mlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // slhDsaSha2Mlkem1024X448PrivateHex, + // true, + // true, + //}, + //{ + // slhDsaShakeMlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // slhDsaShakeMlkem1024X448PrivateHex, + // 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) + } + + 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 test.isSigned { - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) } - if md.Signature == nil { - t.Error("signature missing") + + if test.isSigned { + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } } - } + }) } } @@ -784,7 +877,10 @@ 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, packet.PubKeyAlgoMlkem768P256, + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, + packet.PubKeyAlgoMlkem1024Brainpool384: break default: continue From 11bb422a62ba502b893a57c6d6151f3e4f37c7de Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 19 Jul 2024 15:25:48 +0200 Subject: [PATCH 21/36] Adapt PQC to the v2 API --- go.mod | 6 +- go.sum | 4 + openpgp/v2/key_generation.go | 68 ++++++ openpgp/v2/keys.go | 9 +- openpgp/v2/pqc_vectors_test.go | 217 +++++++++++++++++ openpgp/v2/read.go | 22 +- openpgp/v2/read_test.go | 98 +++++++- openpgp/v2/read_write_test_data.go | 374 +++++++++++++++++++++++++++++ openpgp/v2/subkeys.go | 15 ++ openpgp/v2/write_test.go | 359 +++++++++++++++++---------- 10 files changed, 1023 insertions(+), 149 deletions(-) create mode 100644 openpgp/v2/pqc_vectors_test.go diff --git a/go.mod b/go.mod index a0b64190..0ed3bbb9 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,12 @@ module github.com/ProtonMail/go-crypto go 1.21 -toolchain go1.22.0 - require ( github.com/cloudflare/circl v1.3.7 github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c - golang.org/x/crypto v0.17.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 replace github.com/cloudflare/circl v1.3.7 => github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2 diff --git a/go.sum b/go.sum index 22bf1b54..e5e1461d 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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/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/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= @@ -34,6 +36,8 @@ 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/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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= diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 3029fae7..b7d52d11 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,7 +21,11 @@ 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_ecdsa" + "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" @@ -391,6 +395,49 @@ 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.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, + packet.PubKeyAlgoMldsa87Brainpool384: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 ML-DSA + ECDSA key") + } + + c, err := packet.GetEcdsaCurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_ecdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) + 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) + case packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSA key") + } + + mode, err := packet.GetSlhdsaModeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + parameter := config.SlhdsaParam() + + return slhdsa.GenerateKey(config.Random(), mode, parameter) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -398,6 +445,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() @@ -436,6 +484,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, packet.PubKeyAlgoMldsa65p256, + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, + packet.PubKeyAlgoMldsa87Brainpool384, packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + if pubKeyAlgo, err = packet.GetMatchingMlkemKem(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, packet.PubKeyAlgoMlkem768P256, + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + + 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 192ebbaf..892c5fc1 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -110,6 +110,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 { @@ -117,10 +118,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 } } @@ -212,6 +214,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 @@ -222,10 +225,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/pqc_vectors_test.go b/openpgp/v2/pqc_vectors_test.go new file mode 100644 index 00000000..65d1265f --- /dev/null +++ b/openpgp/v2/pqc_vectors_test.go @@ -0,0 +1,217 @@ +// 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. + +//go:build pqc_test_vectors + +package v2 + +import ( + "bytes" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func dumpTestVector(t *testing.T, filename, vector string) { + t.Logf("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, 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 TestV4EddsaPqKey(t *testing.T) { + //eddsaConfig := &packet.Config{ + // DefaultHash: crypto.SHA512, + // Algorithm: packet.PubKeyAlgoEdDSA, + // V6Keys: false, + // 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: false, + // 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) + //} + + entities, err := ReadArmoredKeyRing(strings.NewReader(v4Ed25519Mlkem768X25519PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + + entity := entities[0] + + serializePqSkVector(t, "v4-eddsa-sample-pk.asc", entity, true) + serializePqPkVector(t, "v4-eddsa-sample-pk.asc", entity, true) + + t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) + } + + var configV1 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: nil, + } + + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1, true) + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2, false) +} + +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 + // }, + //} + // + //entity.Subkeys = []Subkey{} + //err = entity.AddEncryptionSubkey(kyberConfig) + //if err != nil { + // t.Fatal(err) + //} + + entities, err := ReadArmoredKeyRing(strings.NewReader(v6Ed25519Mlkem768X25519PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + + entity := entities[0] + + serializePqSkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + + t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) +} diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index b275130d..b71ad041 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -26,6 +26,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -136,9 +139,10 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, - packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, packet.PubKeyAlgoMlkem1024P384, + packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: break default: continue @@ -741,12 +745,12 @@ func verifyDetachedSignatureReader(keyring KeyRing, signed, signature io.Reader, // checkSignatureDetails verifies the metadata of the signature. // It checks the following: -// - Hash function should not be invalid according to -// config.RejectHashAlgorithms. -// - Verification key must be older than the signature creation time. -// - Check signature notations. -// - Signature is not expired (unless a zero time is passed to -// explicitly ignore expiration). +// - Hash function should not be invalid according to +// config.RejectHashAlgorithms. +// - Verification key must be older than the signature creation time. +// - Check signature notations. +// - Signature is not expired (unless a zero time is passed to +// explicitly ignore expiration). func checkSignatureDetails(pk *packet.PublicKey, signature *packet.Signature, now time.Time, config *packet.Config) error { if config.RejectHashAlgorithm(signature.Hash) { return errors.SignatureError("insecure hash algorithm: " + signature.Hash.String()) diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index d7084b8a..87988f5f 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -12,6 +12,7 @@ import ( "io" "io/ioutil" "os" + "strconv" "strings" "testing" @@ -1003,9 +1004,98 @@ func testMalformedMessage(t *testing.T, keyring EntityList, message string) { } } -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 + v6 bool +}{ + "v4_Ed25519_ML-KEM-768+X25519": { + v4Ed25519Mlkem768X25519PrivateTestVector, + v4Ed25519Mlkem768X25519PublicTestVector, + []string{"b2e9b532d55bd6287ec79e17c62adc0ddd1edd73", "95bed3c63f295e7b980b6a2b93b3233faf28c9d2", "bd67d98388813e88bf3490f3e440cfbaffd6f357"}, + []string{v4Ed25519Mlkem768X25519PrivateV1MessageTestVector, v4Ed25519Mlkem768X25519PrivateV2MessageTestVector}, + false, + }, + "v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + true, + }, +} + +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, !test.v6) + 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 bb383b19..667b78f5 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -758,3 +758,377 @@ jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD =8TxH -----END PGP PRIVATE KEY BLOCK----- ` + +// PQC keys and messages +const v4Ed25519Mlkem768X25519PrivateHex = "c5580451d0c68016092b06010401da470f010107408db47ca20d568541a5af642c5732c9d48b1f6d06099be582763b7982d5bb82580000fe2d90e8f21e63d3e96dd8e816e79e07d526e4939b84bda07412d3f24e3c009b2c122bcd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec28f0413160a0041050251d0c680091077ca82cda8eec0a6162104f9a0bc4d86c90113272d809277ca82cda8eec0a6021b03021e01021901030b090703150a0802160005270902070200007bef0100dc8e6db6e888059aee02d2ac09e813feeb5726fa1f4687b59a3d17667ab02f6400ff44a946bad6674abd35f621f5dfee830d808bdb3f0c5be33f5a156a9f74d9770fc7cd890451d0c6806953228a1963256c93e9f803bbe6785b5b3d0015f38cab3411368a8b30dadf0420ae0419683bbf5a9307e2582bc7ec3efc536eda5c84c3916eb0e8836389846050126322388079abd78bc557e79c8ba3c847a668fae94b42dc8fac584bf1226921eb3a06c18979837e565428200876cad01918a01b27b18ced8c231ad724acc069f1d445f9e3ad139a51b333090967a6c500688fa9cad1908ef6208805e131d92176ac9383ac779a37049e8f9b9227f73e9f26a99c34481a49b3f8a938a49496d2a706ebfba9c8077dfad75950a59d25401241468ab57c4579124635b7b626495232c72473fb5a6635339e6b4b23fb0457f77a6a7c8f7431016de951a3772104388d183b3687b65c5f01833c485f4fe966de10a4f2f71569f431b255b5263691bf45b5a89186efc62ea33c6c05f63afb69a3787a87e029c919d664d5351e96c191df70667c04920e7b1934e58552745c4014cb6ea15d4afc98d046ada704223c3688db549524c1844250030dc83524f5608cb412f0394614aa45a0696af6c8ccf1e1c57718cc6ef138f8b36c37701f06f7b12df13be432905f88c55a799415acc4ae97c33e8357d8d7bdd657551047c3a132a1b0a8c323e35359fbce2bdb1306262913ea7a653c93b897bfbf2485a2c9564b096a6c636296730e1fc888e8362a4efb5670a85024769ca7d6380732abac2b6b3f85ad2a489800f5add0546f2e85602d01cb259b3524b630e362a4aebb41137751dbf5ab0971ce9788b0065458d6e2194de08bf2ec778687902bb572e2e207d4f77f3c824d532c9a31a0a440a2cbb4306c581956ddc7b3e15b019c5187e66a95879c5917432afc139c3858938222a9ec79c64c7579fbf29e55676616989dd36441b1b25b489b66332ca2c9a71dc8850f0c73a20e46096cb4924f24129661509348465b864fe03362960099b00071aef68da14a2eedb735533abfd6d39582663669b962f7266cfd711bd795a15f24b515172a4db788ab3805875a054380608ae204c731a9f10bbd2cf9c735a344912a8fd6f69738b92a257370b1fb3f3ed03c6495b588578b77a29f9d1b23d212615db28b06e004357491b7b8403f54c36bc302a4f998cd08336714b21ca8a98dd7beba6978ba987fb254c5c22b3004d2b2c45b6b7b103aef86b767135e274acc6676a10cfc419344c5053a3ec32740e35c1a597c13f8e74396b3433da42b67a70afd88180c656cbf2763445c34852cb558386f4757a44175000e30bda7773b92fba43090631a6c5ed4f4b51732ad7b864963d02924e06ac2968e2dcbb75322c1fef3c4a307682c16a908d3708592808083c61e604888b53264e44445385dfa6219bcd7c5a5ecb332e363bc2a3bbf719187333bd0b47b04a7a9a308877e31bd1754ba9c990f04f08f61db18b3063bf6d2aaa58a6f431b6921a5adf1b59b1c51a667d21401e8aea11a79a1a521df79ceb7c2596b72c6bfba49da808ff0a9aafec1736f42bc99b2b950e38e50f3ae6c476d4e9032c556237b836cb26263dc0815d0cb3b546733924063a606499b585c0b891d0b817febd9cedf82c26c84abb94a146dec7fc1054716c33409f995bd948ea30a1ec0d77745e89c9277c949b40d0e7b60287026d710a56762966db08b00cc704865ab845c28f6fc7806e675549366a0842b4a43ba9ac93d47e160daa84215549d06f2e20945b5f73d603c13c5ad452390d200ab70af27a845dea5fed96fda6ad403c05fe39764b7e530bc016d40e695a756d388e49e020633b5c876d53b5d28072099803898caa10cf6249dab4031c8a4a3fc61c187791f39ab4e1957adb2bef4aabaa90c8128b1235605892f081f2e658d24535a6c7701ce9a1ea79a20b923563b583d10d9900280ade4c3c436d297066c0e29710f56cb2e32c3113c78ce1c12165a6626ea5c8a01eaab7c0c4e80eb5a342b8d2fd6561f907722909d4cd3b78c1bb6552a63516b5912e5c9cdc75a18699cbcb181f3f37d5865bdf6c04671132b48ca5e98b232ac410bf5926c212cb5258047f6158fded0c881f12995852b2f1cbeef7c0d1c22bd07a80bdec417cc462f0c36467c1caa3af015f8c1753a5a5116406adc33b52657b1fcd87bfb7c7453d51b01db8b68e83c08355235001a78a12cdfc1495b3a26e7518eac828699ea54c9a09b92e4069a3074bcdb0c4c6b7a40d62e6592598984cf4b295265a7370b0b1fb5038efc0302bcc19548e36137410e86622ef0a22b31741f07b9553ad54a907466799a4fe027c2f5b54a0e98a89f2aab17807f4106282279bea94088e7110abf03cbdcd25e3c39694779641903a5efb4bd17b42405e07faaa4728f4a6a161b4e015885f9c17c7182546a3272f60434060a7b59266223f046fd600d79a843f85221a1779ff5807c96b378625c80ef164ad0da095e762ef2b30bc2e195e377832f838f397501e2fc3169ca56a34532c3834949558ddb750023078ec2c85202d292f9b109c1b866cd91c1f7443f2c097d90760f83548507d1cf99a649443a6b517c91933b288c33731b406e8c91cee4852dd4dc1dbe26c9df744f663152e9963daf491ec7073237fc05c44194f1ac3408e430694c4864a96a59bb6d9dd39d02e9cd0bab6754f0252930a40cb73d0a184cb738879d9bc14371797d7b5fa4413c2be555e68c19cb9c84dfd07354d768da8104fff3283686ac8e0780102c5809b033863c211f14c9d2a02452cc7b3ce85fb958b9512c1054b7a341fa471d3104e228c8b2e348685527e545bf1c4448b0829daff4ac2347a883953c1c9ca9830b841382b88a066c4b516d68db184bb473eff1bf970cc2e884a24cd1055f2acd16692f7910917301701741c4bea0466ac31260c22c58415f238cc681848329e0a0d963497b67ca52e3a52352aea5fc8c77e24ee4fa2a46f3a724f1be044a6adc5634d8129c96c5523abc113fac195c837b1067bc21e77a9e68a713a316600a58cc8570dab4cdc540cc414a56d194a36df8c660ca4153a7b329504237aa36a546319a12ae4004aa6d138c6ca51f5c4b5dabcc1be1cb2e8c9120f4112548b8823f2b5aa0026fc05404e77a701295b3e40aad93743fcd791639b06b33e194360b7c3bd82e917c9613fc784662a49a4a270c9b63f9b5161e2a525647b522172ccc627420c97520d11f130561f4a4be9d977a3d32a401e72a51647249b9924ad81ede73473de38e31c8a7ad8aa1e00676df52788f5133d9865ba632a0ed16b0d7596bf659c798bb52a0835f2330446321951639580d319cdcda1ced19ae6b737e6d012374541a44579507515a3182b8852721186197db61bb9686a25383c8e8965a3c888e30bc456fb92dcbb2390bfa1023d6ce1dc261bd1cc79177683d261ee1e9ca182cc80f87b5ae0419683bbf5a9307e2582bc7ec3efc536eda5c84c3916eb0e8836389846050126322388079abd78bc557e79c8ba3c847a668fae94b42dc8fac584bf1226921eb3a06c18979837e565428200876cad01918a01b27b18ced8c231ad724acc069f1d445f9e3ad139a51b333090967a6c500688fa9cad1908ef6208805e131d92176ac9383ac779a37049e8f9b9227f73e9f26a99c34481a49b3f8a938a49496d2a706ebfba9c8077dfad75950a59d25401241468ab57c4579124635b7b626495232c72473fb5a6635339e6b4b23fb0457f77a6a7c8f7431016de951a3772104388d183b3687b65c5f01833c485f4fe966de10a4f2f71569f431b255b5263691bf45b5a89186efc62ea33c6c05f63afb69a3787a87e029c919d664d5351e96c191df70667c04920e7b1934e58552745c4014cb6ea15d4afc98d046ada704223c3688db549524c1844250030dc83524f5608cb412f0394614aa45a0696af6c8ccf1e1c57718cc6ef138f8b36c37701f06f7b12df13be432905f88c55a799415acc4ae97c33e8357d8d7bdd657551047c3a132a1b0a8c323e35359fbce2bdb1306262913ea7a653c93b897bfbf2485a2c9564b096a6c636296730e1fc888e8362a4efb5670a85024769ca7d6380732abac2b6b3f85ad2a489800f5add0546f2e85602d01cb259b3524b630e362a4aebb41137751dbf5ab0971ce9788b0065458d6e2194de08bf2ec778687902bb572e2e207d4f77f3c824d532c9a31a0a440a2cbb4306c581956ddc7b3e15b019c5187e66a95879c5917432afc139c3858938222a9ec79c64c7579fbf29e55676616989dd36441b1b25b489b66332ca2c9a71dc8850f0c73a20e46096cb4924f24129661509348465b864fe03362960099b00071aef68da14a2eedb735533abfd6d39582663669b962f7266cfd711bd795a15f24b515172a4db788ab3805875a054380608ae204c731a9f10bbd2cf9c735a344912a8fd6f69738b92a257370b1fb3f3ed03c6495b588578b77a29f9d1b23d212615db28b06e004357491b7b8403f54c36bc302a4f998cd08336714b21ca8a98dd7beba6978ba987fb254c5c22b3004d2b2c45b6b7b103aef86b767135e274acc6676a10cfc419344c5053a3ec32740e35c1a597c13f8e74396b3433da42b67a70afd88180c656cbf2763445c34852cb558386f4757a44175000e30bda7773b92fba43090631a6c5ed4f4b51732ad7b864963d02924e06ac2968e2dcbb75322c1fef3c4a307682c16a908d3708592808083c61e604888b53264e44445385dfa6219bcd7c5a5ecb332e363bc2a3bbf719187333bd0b47b04a7a9a308877e31bd1754ba9c990f04f08f61db18b3063bf6d2aaa58a6f431b6921a5adf1b59b1c51a667d21401e8aea11a79a1a521df79ceb7c2596b72c6bfba49da808ff0a9aafec1736f42bc99b2b950e38e50f3ae6c476d4e9032c556237b836cb26263dc0815d0cb3b546733924063a606499b585c0b891d0b817febd9cedf82c26c84abb94a146dec7fc1054716c33409f995bd948ea30a1ec0d77745e89c9277c949b40d0e7b60287026d710a56762966db08b00cc704865ab845c28f6fc7806e675549366a0842b4a43ba9ac93d47e160daa84215549d06f2e20945b5f73d603c13c5ad452390d2b28c44046b6117559a75c255de0944a4699b34fd2bd4859fb261d9eb7e445823c6ad5f7a19ba7c36da8d10918aad8373d3e43ab24a020980736fda04cbdfe98d6f5ac2780418160a002a050251d0c680091077ca82cda8eec0a6162104f9a0bc4d86c90113272d809277ca82cda8eec0a6021b0c000051b80100f05de3a23334bebca47402a4e7cb23900ab5c365cf4b0fb7cd1381690763f5010100aaa0984cb139e3c1c3a51aa577ec32b4d42e44bfc8f9cfff5dc8459cc05e8d0b" + +const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v4Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +0O22blDNAr/tDA== +=q5En +-----END PGP PRIVATE KEY BLOCK-----` + +const v4Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=dPFW +-----END PGP PUBLIC KEY BLOCK-----` + +const v4Ed25519Mlkem768X25519PrivateV1MessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPUA+RAz7r/1vNXaUNGH8CAkSiFgunnUDqAiD9JSd3Sb7lMNUsWk6lzWiJicgky +S/vu0sSnRtxweWkoMr1y2ZaS45nXbEQyShiqHhZUKfVwtxbU+rGVH5oCgSvtTCrs +verZaFpqzqPWyZ8ApzJvjbGUDBuwns09dGIKvKoePT5DCrqXlsW4EA8gFJbiXeb3 +E7nsyg3l2uMzbt6FHtYoa6qq9Q0PsUiGte52nXXWEnmBOGUfmCkVsgmHDmz63BLT +1xXuZ5YopZkhhpjTNtvWtXc6MIaqnh6XtAcg8ZoaH0iferpbHEp9+M4bv5YDjzji +vv83rBQN4cBaS1/TSmBkNJHmxcyT1AOOXY2ZbmxQBORhGOTrFz3w8R78MYkEvB6x +JAjoYirpsyNLJzdewpXEYrPQq4Ey8EG2+qDY47vQkQaYcSFFoxYQ8MpHXmmgJ2bp +D13g/lQlSHcdWX2L59Wa1dhKRVnUyeEtO5c06FKJ7QOrywNjPdVciPVCx6bBfVd2 +6qiWLynSGnzGaKd1YyaviioCm48Ydu5q8Z+QbEANbKW1azVAWCuxuiomE3RBvf1O +8d30UvBnImEf+9ANDxzmjIG2lW39U591Jbv0pL00at3tIMQN2wwiduP1KZ1dilWa +gEkdPjl6Q68ov0vRCYMAZizj4pMZbsUdge2Jj9GieObnp+w25pJu9nBeI6iqYmwd +Ny1U3OuvzbEUsNfKcHoQd9Cem8EZn+5ICk7eqsTkZq69oYfIVRyzEEc/X9562nzh +6B+X4CHZY/C8UCWougQriG4KVszM4myOgekKg0kNVIWgE2y7Z//S9c2twdxRWT/a +8QC4p7QX7JRgzDD9erkj/9J3hKwHxDHShKB5jsVaGO+BxtFSCiiTmgeo7+SAnJwU +Mi/N0UiI2BbKdo4KmdDPUVDyobBjCjeXil7Kg7pTU0vewPZQDLl9X16CcXCB60HL +fkDGpcYbjkZYbmB449sQfaLvxRMHomP4TY4PEfANIXdWmk1mS0/+zNzMQ9+Xderc +8P/EdKDKF5yr7IzSNoxuLiIWpyWJj+5QmAwup9mVv5gkh5RPnUQ0fgQ1vU8K9PMz +OmYqlX2W4gPn29UovjkbGH+lEzazEzA7VZWHXG86NVN8WMXqdQvMJcmMRZhDmC3F +kCII5zc6dxFXjNUgaAqV8eBqvRBbgCqK+6HSwCMY7jNFhFIy+Nj/9BYU/ereax0t +Zlsk7XDK9lMZUidh5+VeEqbyMsLQ0YiyO7VJ5VdiPESXHjPkzxo42XZJELuBVC9D +ArAX2Qip+oV1RXzhu/SeJdRQufGSENeZpGiG4tW24dpROh40I5TgXmpd4ALhuh1S +PrepCNhXuFtKDIStKZEmCknPAGWAkLYZz5rAaMtztdGvzlektn+8CDtSo3d6FUww +dp68ZtSMMb5HGscAoiDoOTiB5KVPSd80s3EPXlsgQSfHuSUHTvmD8G6q4hqGXMeV +IUdwjwTvDMfW7CU5zqiV01SO6dXKsFyjLJrT57kpCbQ/2fhoMC+kNcXpzI+Z65yI +jCP6Sjv+cVh7tv55kTKAPHO5VE3MDxvSOQHpUQ0zora+lfzpLUahfv8uZ4Q4J3L6 +mkHfXuplyv3LcunejQDog2bhakqbrb5lg3fZGYNagykZxw== +=2Xhi +-----END PGP MESSAGE-----` + +const v4Ed25519Mlkem768X25519PrivateV2MessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPhBhUEvWfZg4iBPoi/NJDz5EDPuv/W81dpZ1Yz1yu1Dk/HK2JuEmE6RavqzhvT +i508AZhPxC08BxfNFar+uyZCNyMrUSrY0qY8H61GTtx1+O9VynXl8uXtS1nTDGJ9 +vCR+EvH6rT/gOPQB8HUhX6Ps97Yqi/Iys1gfS8n961pScwIYpPJzUWfUUKjIT55W +htkh9aIB6unqzwUDi3p4oRZRm67j1ZP14SLyonAG2tXtCZyu1An62UHeOyNl1/6Z +CgC3egTf6lz26US15T8AP54AO77LOf9KwLpUYcwvSExqHGgmhS0Mil6WnFyuJUDB +7A2T2p/koW7TDaqoxhWsxY2isiH1SmAxNxzMnrGd7rNpPJ/k/r42bILfOuG0TRUN +zqC9ph6OdydSyhHkN5G4eOYQqqvk19/lfLuHWlNwfNcn/2PsgsxLxNj7ltVn90W0 +qLubPWrujn/DhLl+hs2xXDOudpcztUqxcBnrsSaHlaebjQoDfttVAQj2jjdNXRjZ +uNRnRfcG9s3sO3b8d4ed6tk6U+nMrE2dZCBjTagqvD07Z1TpZDh7t86V3X16o/ps +jxW42s+YR589b88IZcieZRbKVtXt00pn2tn95kpvL3d8nAkaiPUhrowQUz0jpn8c +CDBNAn1j690qM3pD5XJlwverC2cmJH1Hjobnrhi6X1k2lQxweX28p+R9NQjSoX0h +ORuE0/Wpi15y0xmr2EzjcZ/6vPncy/IrYJCYmx9+aWQAjrKjizzNFTt73kf1xba5 +t4tbZkj9xgdDJXq3bAqB0/JeeTb4aTCk+n4olVYzCnMtLgj+1fWPClMModACmFOG +1+bw5Q91/7euo363sw5UwgU1JhSQ/xcKNyJQsnklWkLMJNB1Yhj/C32lEmLntigv +UOO510+ehA7D5ftef8cMfEIm73HrBBiLfixvVTR8AQV4hiV/mzKP7weM7kxvAvbz +ir4jt3uSBOuhTjzq2is/S3D2K+O8FZqGIbkDhnKd98LbEA2cn9nTfsbV+TVXCmaS +lHNojVxPL2pUKxedV5skvfflRFciuP7UNsf8myHe7wdfPdSzMsbytDEwID3vcsme +fBqZdEZxqv/mNnn38TfHMSCF+yv5XbF9ham4DIcqNlkYud1ipEFFbcBZ0o9nUIWp +diSY7KGAtVF224dtcr3FTHGuBnayDq+Yk++VhF4Bb3uPVuwrkf7Bncp1aYEQfkhI +HwF3X6GnwC3y7kpbkU1rOq7yXv/0mRyGpVQlW/Yf3qT1buxcWt5BvXBmKzbBpVg/ +0B9vpzrlFsT0Pb2GHuQ6U+9JoZ+ePnRMVdDz93RCGr1kQlyY15K1b+yILJiV6oOL +OxoxXHnr5soIumxCqv+6oAm4SdQVJLELQK72x1dVKJ90jUOgYCeOY61NsC9BFWHT +h0itUEnwWMjKg73z00bthndwfEXHBJLrHizkcv+pwD8M5wb/9H6HU4x8ELSr5Fyn +WjSoa2739wmJkoJY5ifaic3L8UXJeLuEZnVG9tUrl9ohHO8RNR3Vc/uHmyhImoYp +RL4rcc6YpuyextmYu9S9LkPR5Bzr+mFeJDeXbA7GJm9eofdw0lQCCQIMAGc2j84/ +tfivyP5YrgQ8uBt9iwJN3IYRBy8qdr9JUyxkpkOEshV6XE4g3Orpbx0ZdrxbKmDS +7eJl5fSust3gb2KfaAoWkFQivVJP2KTl5gw= +-----END PGP MESSAGE-----` + +const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +/d8lkIgM +-----END PGP PRIVATE KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xioGUdDGgBsAAAAgsJV1qyvdl+EenEB4IFvP5/7Ci5XJ1rk8Yh967qV1rb3CrwYf +GwoAAABABQJR0MaAIqEGUjQyQjRSVAUCGc7/KG6cjkeeyIdX+VNUOImEoC19C1kC +GwMCHgkDCwkHAxUKCAIWAAUnCQIHAgAAAADhOyBW8CPDe5FreFmlonhfVhr2EPw3 +WFLyd6mKRhkQm3VBfw7Qw7eermL9Cr5O7Ah0JxmIkT18jgKQr9AwWa3nm2mcbjSo +ib2WVzm5EiW3f3lgflfrySQFpSICzPl2QcAcrgjNLlBRQyB1c2VyIChUZXN0IEtl +eSkgPHBxYy10ZXN0LWtleUBleGFtcGxlLmNvbT7CmwYTGwoAAAAsBQJR0MaAIqEG +UjQyQjRSVAUCGc7/KG6cjkeeyIdX+VNUOImEoC19C1kCGQEAAAAAg2ogTEbKVVlb +WsejQHkq7xo8ipM7dv6Hz2AekkJqupKVR+/oy+2j6ri+/B2K6k1v1y5quzirhs87 +fB5AxZC6ZoFDvC0kZOvo14fPF07wCx0jwJVOWuRFVsVw7pQJHbNzgkIAzsQKBlHQ +xoBpAAAEwLRbSSpvve2pIh3hHweqq2VdRo+7Zf7whYHyXM/UifsniwMKSrubvsmL +gCyiEwMip3ZlTSxIFDaFEMVtVvCSJ7XFZ0WslTJnZ/CENPgxbVgn6CC2b8UEb8ol +S3AxlSiqJSRP0OrOJdfPWJI1A+p7Vmw1CZQq2oVPUlE96SVUrFxfk7XCYpcTpIQb ++mFB4ULCesat5tud7TauUJpMKssUf0I74EUjahoR46pPReKzlSqfvhpgXSASZpBg +8IZBY7VbgTnLInGTTnErrScVlDnAwcdYvuZMQYO5EjS6LOxn1aVfU+iH+Rir2AyF +zsYl6ICHciPAsKKa+Sk7UPFBrIRG1qgn7FF0n5epHeiFCRNb87wSqlp0h+d8L3jP +mDq4zoQPKDViasoHYXLD7KoJTIxP2eGzjMRlg3oD9ph3ZnyOTIsx/4SDtxW3q+JU +8RFoI0dZEdURwaoIITWitldtPUmtBuJshceEDSWopuwLzBuVTnYDpTy94ZtDBKmg +PnmSmPOKZ6THucmiJGUmWmAKkyo7kWAwYRsE2ZYqLzIJFmZFzRLIThipiZhR/9h2 +GemQklMJqYs25cEGx6FWzXRv8Palm7yOAicH/ldHUOtU3oFIXthOatwSrQApJ7HH +vksx59ZtLFtBgHm5eRmYYleJsJLGCPssa7pK2hIwgLlmCLSAavFqYjuocWIYKLmw +5vNXXRWIjPBbTpVXbUO5U9F/67gggSWBJXCZlfgcluO422aN22m8aONiTgZtmjcC +2elci5yRKGBbeKmFTcVsZbpbY6ZCKFRyzbqmMGYe0mqN6lh7R5dNiBuJZQg04mYu +SzWCF3mumlJTRtlN9Miy6LyWApJSTQdgc3awS0mjUrgU1Ia0AjMFKcxJA6iHd6iA +xWMbUqxOSoTOTUlMr3ltpaNGEMGpaHwMoQs99xSI1zG9pYmfeIl6LfZSwnI4LsBv +NOBiUhNUC/aYIILEm7qjTpw5YdI+6jSl+palLlcMDzt0LgMN8rY6UlZJBGNFSAKS +NSWXdFYMByKKGSCj91TDWPlOLvWKntSLk5eLodhgmRGqx5GZECgWS4wDARY00rl1 +7dV53GejXrUtJaYcnam5pKoTSaPJTuY25Kyy+oB7aHpV0vA87JaeRCsqkjcS5IQK +dtceUskXNRa2f7CTrfQRhOGk0gSA4Jx8+Fw8uGWLGJx6m1lSyWcMX5HL7hJkFhEK +ebYjdALGXMV1wxNiUHCIvxCjX/AkwHEDvAN6qhULrcZlmngSbeBysOFud2a8PIS2 +p7RCAatO+TpFgoR+1CgVJIdiRpM0WrMfS9iBERhtYaLH1oUjBpcV7zpgNdkT4Clf +bTpgu3oPnWBogDjMXKUepSfFx0l1tNGRLCCFVit8xxA4Q+phutInyXUAHJiEfHIR +4jxTd/FwQ3pDoKxTesY+XsGtVJxe9oMrXSlt6uymn6zKQlQsw8odvHhp5/NWqkCh +9/xQvmIlERsVVjyJ0FNF/+HNT9KrECCj6+cujDbEN6UmRlFvlMcxFzYaTnWa1csh +SVCCa1aYZddWrDdxOwMfObUw8TukY7A2RqcdpmpA68SLoWwNAgtFG1xWV43yC/P3 +XTsqTmgHRUGboDkVs9K81+Byg4jhKWfCmwYYGwoAAAAsBQJR0MaAIqEGUjQyQjRS +VAUCGc7/KG6cjkeeyIdX+VNUOImEoC19C1kCGwwAAAAA5kEgPwatbx3FHPIy9J9m +GUEpUE03oRRPE8N4lJ2eAIMhciCEHp3BzYVGvW3OaPYmjcu4JTREPJM6HP7yR+ZE +g+Bld9lBSVmEdMJnOX2ZHOdEoRV4bm1U4aPuhrKL/d8lkIgM +-----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 9dc70899..e60ef427 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -208,3 +208,18 @@ 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.PubKeyAlgoMlkem768P256, + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384, + packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + return true + default: + return false + } + +} diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index 28550862..3d834263 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,255 @@ 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, + }, + "v4_Ed25519_ML-KEM-768+X25519": { + v4Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v4_Ed25519_ML-KEM-768+X25519_signed": { + v4Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "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, + }, + //{ + // mldsa87Ed448Mlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87Ed448Mlkem1024X448PrivateHex, + // true, + // true, + //}, + //{ + // mldsa65P256Mlkem768P245PrivateHex, + // false, + // true, + //}, + //{ + // mldsa65P256Mlkem768P245PrivateHex, + // true, + // true, + //}, + //{ + // mldsa87P384_Mlkem1024P384PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87P384_Mlkem1024P384PrivateHex, + // true, + // true, + //}, + //{ + // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, + // false, + // true, + //}, + //{ + // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, + // true, + // true, + //}, + //{ + // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, + // false, + // true, + //}, + //{ + // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, + // true, + // true, + //}, + //{ + // slhDsaSha2Mlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // slhDsaSha2Mlkem1024X448PrivateHex, + // true, + // true, + //}, + //{ + // slhDsaShakeMlkem1024X448PrivateHex, + // false, + // true, + //}, + //{ + // slhDsaShakeMlkem1024X448PrivateHex, + // 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) + } + } + + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } - if test.isSigned { - if !md.IsVerified { - t.Errorf("not verified despite all data read") + encryptKey, out := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + if !out { + t.Fatalf("#%s: No encryption key found", name) } - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + 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.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") + } + } + }) } } From bd632913e593fb545f44ad31a6cdf25bc0222f7a Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 19 Jul 2024 15:38:40 +0200 Subject: [PATCH 22/36] Remove sphincs PQC logic --- go.mod | 1 - openpgp/benchmark_v6_test.go | 110 +++++++---------------- openpgp/key_generation.go | 15 +--- openpgp/keys.go | 7 +- openpgp/keys_v6_test.go | 7 -- openpgp/packet/config.go | 10 --- openpgp/packet/packet.go | 5 +- openpgp/packet/private_key.go | 45 ---------- openpgp/packet/public_key.go | 104 +--------------------- openpgp/packet/signature.go | 47 +--------- openpgp/slhdsa/parameter.go | 85 ------------------ openpgp/slhdsa/sphincs.go | 155 --------------------------------- openpgp/slhdsa/sphincs_test.go | 124 -------------------------- openpgp/v2/key_generation.go | 15 +--- openpgp/v2/subkeys.go | 3 +- 15 files changed, 40 insertions(+), 693 deletions(-) delete mode 100644 openpgp/slhdsa/parameter.go delete mode 100644 openpgp/slhdsa/sphincs.go delete mode 100644 openpgp/slhdsa/sphincs_test.go diff --git a/go.mod b/go.mod index 0ed3bbb9..43bad186 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.21 require ( github.com/cloudflare/circl v1.3.7 - github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c golang.org/x/crypto v0.25.0 ) diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go index ec3db054..e40208f1 100644 --- a/openpgp/benchmark_v6_test.go +++ b/openpgp/benchmark_v6_test.go @@ -12,22 +12,22 @@ import ( const benchmarkMessageSize = 1024 // Signed / encrypted message size in bytes -var benchmarkTestSet = map[string] *packet.Config { +var benchmarkTestSet = map[string]*packet.Config{ "RSA_1024": { Algorithm: packet.PubKeyAlgoRSA, - RSABits: 1024, + RSABits: 1024, }, "RSA_2048": { Algorithm: packet.PubKeyAlgoRSA, - RSABits: 2048, + RSABits: 2048, }, "RSA_3072": { Algorithm: packet.PubKeyAlgoRSA, - RSABits: 3072, + RSABits: 3072, }, "RSA_4096": { Algorithm: packet.PubKeyAlgoRSA, - RSABits: 4096, + RSABits: 4096, }, "Ed25519_X25519": { Algorithm: packet.PubKeyAlgoEd25519, @@ -37,27 +37,27 @@ var benchmarkTestSet = map[string] *packet.Config { }, "P256": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveNistP256, + Curve: packet.CurveNistP256, }, "P384": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveNistP384, + Curve: packet.CurveNistP384, }, "P521": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveNistP521, + Curve: packet.CurveNistP521, }, "Brainpool256": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveBrainpoolP256, + Curve: packet.CurveBrainpoolP256, }, "Brainpool384": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveBrainpoolP384, + Curve: packet.CurveBrainpoolP384, }, "Brainpool512": { Algorithm: packet.PubKeyAlgoECDSA, - Curve: packet.CurveBrainpoolP512, + Curve: packet.CurveBrainpoolP512, }, "ML-DSA3Ed25519_ML-KEM768X25519": { Algorithm: packet.PubKeyAlgoMldsa65Ed25519, @@ -77,54 +77,6 @@ var benchmarkTestSet = map[string] *packet.Config { "ML-DSA5Brainpool384_ML-KEM1024Brainpool384": { Algorithm: packet.PubKeyAlgoMldsa87Brainpool384, }, - "SLH-DSA-SHA2_128s_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 1, - }, - "SLH-DSA-SHA2_128f_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 2, - }, - "SLH-DSA-SHA2_192s_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 3, - }, - "SLH-DSA-SHA2_192f_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 4, - }, - "SLH-DSA-SHA2_256s_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 5, - }, - "SLH-DSA-SHA2_256f_ML-KEM1024X448": { - Algorithm: packet.PubKeyAlgoSlhdsaSha2, - SlhdsaParameterId: 6, - }, - "SLH-DSA-SHAKE_128s_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 1, - }, - "SLH-DSA-SHAKE_128f_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 2, - }, - "SLH-DSA-SHAKE_192s_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 3, - }, - "SLH-DSA-SHAKE_192f_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 4, - }, - "SLH-DSA-SHAKE_256s_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 5, - }, - "SLH-DSA-SHAKE_256f_ML-KEM1024X448":{ - Algorithm: packet.PubKeyAlgoSlhdsaShake, - SlhdsaParameterId: 6, - }, } func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { @@ -192,10 +144,10 @@ func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) var signed *Entity if sign { - signed = keys[n % len(keys)] + signed = keys[n%len(keys)] } - w, err := Encrypt(buf, EntityList{keys[n % len(keys)]}, signed, nil, config) + w, err := Encrypt(buf, EntityList{keys[n%len(keys)]}, signed, nil, config) if err != nil { b.Errorf("Failed to initalize encryption: %s", err) continue @@ -222,8 +174,8 @@ func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) 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) + 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 @@ -257,7 +209,7 @@ func benchmarkSign(b *testing.B, keys []*Entity, plaintext []byte) [][]byte { for n := 0; n < b.N; n++ { buf := new(bytes.Buffer) - err := DetachSign(buf, keys[n % len(keys)], bytes.NewReader(plaintext), nil) + err := DetachSign(buf, keys[n%len(keys)], bytes.NewReader(plaintext), nil) if err != nil { b.Errorf("Failed to sign: %s", err) continue @@ -273,9 +225,9 @@ func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures b.ResetTimer() for n := 0; n < b.N; n++ { signed := bytes.NewReader(plaintext) - signature := bytes.NewReader(signatures[n % len(signatures)]) + signature := bytes.NewReader(signatures[n%len(signatures)]) - parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n % len(keys)]}, signed, signature,nil) + parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n%len(keys)]}, signed, signature, nil) if signatureError != nil { b.Errorf("Signature error: %s", signatureError) @@ -292,63 +244,63 @@ func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures } 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) + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + b.Run("Verify_"+name, func(b *testing.B) { benchmarkVerify(b, keys, plaintext[:], signatures[name]) }) } diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index de801158..e033cc9c 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -15,7 +15,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" @@ -348,18 +347,6 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) - case packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: - if !config.V6() { - return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSA key") - } - - mode, err := packet.GetSlhdsaModeFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - parameter := config.SlhdsaParam() - - return slhdsa.GenerateKey(config.Random(), mode, parameter) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -408,7 +395,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384, packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + packet.PubKeyAlgoMldsa87Brainpool384: if pubKeyAlgo, err = packet.GetMatchingMlkemKem(config.PublicKeyAlgorithm()); err != nil { return nil, err } diff --git a/openpgp/keys.go b/openpgp/keys.go index 131c30b4..6b1da154 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -314,10 +314,9 @@ func (s *Subkey) Revoked(now time.Time) bool { func (s *Subkey) IsPQ() bool { switch s.PublicKey.PubKeyAlgo { case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, - packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384, - packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384: return true default: return false diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index 28009667..2f218982 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -12,7 +12,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "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/packet" ) @@ -219,8 +218,6 @@ func TestGeneratePqKey(t *testing.T) { "ML-DSA87_P384": packet.PubKeyAlgoMldsa87p384, "ML-DSA65_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, "ML-DSA87_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, - "Slhdsa_simple_SHA2": packet.PubKeyAlgoSlhdsaSha2, - "Slhdsa_simple_SHAKE": packet.PubKeyAlgoSlhdsaShake, } for name, algo := range asymmAlgos { @@ -284,10 +281,6 @@ func TestGeneratePqKey(t *testing.T) { pk.PublicMldsa = pk.Mldsa.PublicKeyFromBytes(bin) } - if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*slhdsa.PublicKey); ok { - pk.PublicData.PKseed[5] ^= 1 - } - err = read.PrivateKey.Decrypt(randomPassword) if _, ok := err.(errors.KeyInvalidError); !ok { t.Fatal("Failed to detect invalid ML-DSA key") diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index e52fa55a..fb21e6d1 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -7,7 +7,6 @@ package packet import ( "crypto" "crypto/rand" - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "io" "math/big" "time" @@ -94,8 +93,6 @@ type Config struct { // Curve configures the desired packet.Curve if the Algorithm is PubKeyAlgoECDSA, // PubKeyAlgoEdDSA, or PubKeyAlgoECDH. If empty Curve25519 is used. Curve Curve - // SlhdsaParameterId configures the desired sphincs plus security level parameter. - SlhdsaParameterId slhdsa.ParameterSetId // AEADConfig configures the use of the new AEAD Encrypted Data Packet, // defined in the draft of the next version of the OpenPGP specification. // If a non-nil AEADConfig is passed, usage of this packet is enabled. By @@ -266,13 +263,6 @@ func (c *Config) S2K() *s2k.Config { return c.S2KConfig } -func (c *Config) SlhdsaParam() slhdsa.ParameterSetId { - if c == nil || c.SlhdsaParameterId == 0 { - return slhdsa.Param128f - } - return c.SlhdsaParameterId -} - func (c *Config) AEAD() *AEADConfig { if c == nil { return nil diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index d7ab7416..1368ff46 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -528,8 +528,6 @@ const ( PubKeyAlgoMldsa87p384 = 38 PubKeyAlgoMldsa65Brainpool256 = 39 PubKeyAlgoMldsa87Brainpool384 = 40 - PubKeyAlgoSlhdsaSha2 = 109 - PubKeyAlgoSlhdsaShake = 42 ) // CanEncrypt returns true if it's possible to encrypt a message to a public @@ -550,8 +548,7 @@ func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, - PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384, - PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index aeff67fe..b6412660 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -22,7 +22,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" "github.com/cloudflare/circl/kem/mlkem/mlkem768" "github.com/cloudflare/circl/sign/dilithium" @@ -182,8 +181,6 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewMldsaEcdsaPublicKey(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") } @@ -604,18 +601,6 @@ func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) er return err } -// serializeSlhdsaPrivateKey serializes a SLH-DSA private key according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 -func serializeSlhdsaPrivateKey(w io.Writer, priv *slhdsa.PrivateKey) error { - privateData, err := priv.SerializePrivate() - if err != nil { - return err - } - - _, err = w.Write(encoding.NewOctetArray(privateData).EncodedBytes()) - return err -} - // decrypt decrypts an encrypted private key using a decryption key. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -926,9 +911,6 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeMldsaEcdsaPrivateKey(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") } @@ -975,8 +957,6 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseMldsaEcdsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: return pk.parseMldsaEcdsaPrivateKey(data, 48, dilithium.MLDSA87.PrivateKeySize()) - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - return pk.parseSlhdsaPrivateKey(data) default: err = errors.StructuralError("unknown private key type") return @@ -1397,31 +1377,6 @@ func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, kLen int) (er return nil } -// parseSlhdsaPrivateKey parses a SLH-DSA private key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 -func (pk *PrivateKey) parseSlhdsaPrivateKey(data []byte) (err error) { - if pk.Version != 6 { - return goerrors.New("openpgp: cannot parse non-v6 SLH-DSA key") - } - pub := pk.PublicKey.PublicKey.(*slhdsa.PublicKey) - priv := new(slhdsa.PrivateKey) - priv.PublicKey = *pub - - buf := bytes.NewBuffer(data) - spx := encoding.NewEmptyOctetArray(priv.ParameterSetId.GetSkLen()) - if _, err := spx.ReadFrom(buf); err != nil { - return err - } - - priv.UnmarshalPrivate(spx.Bytes()) - if err := slhdsa.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 08a4947f..85ac9e49 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -24,7 +24,6 @@ import ( "github.com/ProtonMail/go-crypto/brainpool" "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" "github.com/cloudflare/circl/kem/mlkem/mlkem768" @@ -66,9 +65,6 @@ type PublicKey struct { // kdf stores key derivation function parameters // used for ECDH encryption. See RFC 6637, Section 9. kdf encoding.Field - - // slhDsaParameterSetId contains the parameter set ID for the SLH-DSA instantiation - slhDsaParameterSetId slhdsa.ParameterSetId } // UpgradeToV5 updates the version of the key to v5, and updates all necessary @@ -335,27 +331,6 @@ func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) return pk } -func NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKey { - var pk *PublicKey - - publicData, err := pub.SerializePublic() - if err != nil { - panic("generated invalid SLH-DSA public key") - } - - pk = &PublicKey{ - Version: 6, - CreationTime: creationTime, - PubKeyAlgo: GetAlgIDFromSlhdsaMode(pub.Mode), - PublicKey: pub, - p: encoding.NewOctetArray(publicData), - slhDsaParameterSetId: pub.ParameterSetId, - } - - pk.setFingerprintAndKeyId() - return pk -} - func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -426,10 +401,6 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseMldsaEcdsa(r, 65, dilithium.MLDSA65.PublicKeySize()) case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: err = pk.parseMldsaEcdsa(r, 97, dilithium.MLDSA87.PublicKeySize()) - case PubKeyAlgoSlhdsaSha2: - err = pk.parseSlhdsa(r, slhdsa.ModeSimpleSHA2) - case PubKeyAlgoSlhdsaShake: - err = pk.parseSlhdsa(r, slhdsa.ModeSimpleShake) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -887,39 +858,6 @@ func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { return } -// parseSlhdsa parses a SLH-DSA public key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-3 -func (pk *PublicKey) parseSlhdsa(r io.Reader, mode slhdsa.Mode) (err error) { - var id slhdsa.ParameterSetId - pub := new(slhdsa.PublicKey) - - var param [1]byte - if _, err = readFull(r, param[:]); err != nil { - return - } - - if id, err = slhdsa.ParseParameterSetID(param); err != nil { - return - } - - pk.slhDsaParameterSetId = id - pub.ParameterSetId = id - pub.Mode = mode - pub.Parameters, err = slhdsa.GetParametersFromModeAndId(mode, id) - - pk.p = encoding.NewEmptyOctetArray(pub.ParameterSetId.GetPkLen()) - if _, err = pk.p.ReadFrom(r); err != nil { - return - } - - if err := pub.UnmarshalPublic(pk.p.Bytes()); err != nil { - return err - } - - pk.PublicKey = pub - return -} - // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -1016,9 +954,6 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: length += uint32(pk.p.EncodedLength()) length += uint32(pk.q.EncodedLength()) - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - length += 1 // ParamID octet - length += uint32(pk.p.EncodedLength()) default: panic("unknown public key algorithm") } @@ -1136,12 +1071,6 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(pk.q.EncodedBytes()) return - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - if _, err = w.Write(pk.slhDsaParameterSetId.EncodedBytes()); err != nil { - return - } - _, err = w.Write(pk.p.EncodedBytes()) - return } return errors.InvalidArgumentError("bad public-key algorithm") } @@ -1252,13 +1181,6 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("mldsa_ecdsa verification failure") } return nil - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - spxPublicKey := pk.PublicKey.(*slhdsa.PublicKey) - if sig.slhDsaParameterSetId != spxPublicKey.ParameterSetId || - !slhdsa.Verify(spxPublicKey, hashBytes, sig.SlhdsaSig.Bytes()) { - return errors.SignatureError("SLH-DSA verification failure") - } - return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1494,8 +1416,6 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: bitLength = pk.q.BitLength() // Very questionable - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - bitLength = pk.p.BitLength() // Even more questionable default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1539,7 +1459,7 @@ func GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { switch algId { case PubKeyAlgoMldsa65Ed25519: return PubKeyAlgoMlkem768X25519, nil - case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + case PubKeyAlgoMldsa87Ed448: return PubKeyAlgoMlkem1024X448, nil case PubKeyAlgoMldsa65p256: return PubKeyAlgoMlkem768P256, nil @@ -1612,28 +1532,6 @@ func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { } } -func GetSlhdsaModeFromAlgID(algId PublicKeyAlgorithm) (slhdsa.Mode, error) { - switch algId { - case PubKeyAlgoSlhdsaSha2: - return slhdsa.ModeSimpleSHA2, nil - case PubKeyAlgoSlhdsaShake: - return slhdsa.ModeSimpleShake, nil - default: - return 0, goerrors.New("packet: unsupported EdDSA public key algorithm") - } -} - -func GetAlgIDFromSlhdsaMode(mode slhdsa.Mode) PublicKeyAlgorithm { - switch mode { - case slhdsa.ModeSimpleSHA2: - return PubKeyAlgoSlhdsaSha2 - case slhdsa.ModeSimpleShake: - return PubKeyAlgoSlhdsaShake - default: - panic("invalid SLH-DSA mode") - } -} - func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (dilithium.Mode, error) { switch algId { case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 7f154d9a..47916e58 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -16,7 +16,6 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" "github.com/cloudflare/circl/sign/dilithium" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" @@ -74,9 +73,6 @@ type Signature struct { MldsaSig encoding.Field SlhdsaSig encoding.Field - // slhDsaParameterSetId contains the parameter set ID for the SLH-DSA instantiation - slhDsaParameterSetId slhdsa.ParameterSetId - // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -186,8 +182,7 @@ func (sig *Signature) parse(r io.Reader) (err error) { switch sig.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, - PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384, - PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: + PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -346,10 +341,6 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err = sig.parseMldsaEcdsaSignature(r, 48, dilithium.MLDSA87.SignatureSize()); err != nil { return } - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - if err = sig.parseSlhdsaSignature(r); err != nil { - return - } default: panic("unreachable") } @@ -387,23 +378,6 @@ func (sig *Signature) parseMldsaEcdsaSignature(r io.Reader, ecLen, dLen int) (er return } -// parseSlhdsaSignature parses a SLH-DSA signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-packet-tag-2-2 -func (sig *Signature) parseSlhdsaSignature(r io.Reader) (err error) { - var param [1]byte - if _, err = readFull(r, param[:]); err != nil { - return - } - - if sig.slhDsaParameterSetId, err = slhdsa.ParseParameterSetID(param); err != nil { - return - } - - sig.SlhdsaSig = encoding.NewEmptyOctetArray(sig.slhDsaParameterSetId.GetSigLen()) - _, err = sig.SlhdsaSig.ReadFrom(r) - return -} - // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 4880, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -1073,17 +1047,6 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e sig.MldsaSig = encoding.NewOctetArray(dSig) sig.EdDSASigR = encoding.NewOctetArray(ecSig) } - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - if sig.Version != 6 { - return errors.UnsupportedError("cannot use SLH-DSA on a non-v6 signature") - } - sk := priv.PrivateKey.(*slhdsa.PrivateKey) - spxSig, err := slhdsa.Sign(sk, digest) - - if err == nil { - sig.slhDsaParameterSetId = sk.ParameterSetId - sig.SlhdsaSig = encoding.NewOctetArray(spxSig) - } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1220,9 +1183,6 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = int(sig.ECDSASigR.EncodedLength()) sigLength += int(sig.ECDSASigS.EncodedLength()) sigLength += int(sig.MldsaSig.EncodedLength()) - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - sigLength = 1 // Parameter ID - sigLength += int(sig.SlhdsaSig.EncodedLength()) default: panic("impossible") } @@ -1345,11 +1305,6 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.MldsaSig.EncodedBytes()) - case PubKeyAlgoSlhdsaSha2, PubKeyAlgoSlhdsaShake: - if _, err = w.Write(sig.slhDsaParameterSetId.EncodedBytes()); err != nil { - return - } - _, err = w.Write(sig.SlhdsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/slhdsa/parameter.go b/openpgp/slhdsa/parameter.go deleted file mode 100644 index 67d63a72..00000000 --- a/openpgp/slhdsa/parameter.go +++ /dev/null @@ -1,85 +0,0 @@ -// Package slhdsa implements SLH-DSA suitable for OpenPGP, experimental. -// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-2 -package slhdsa - -import ( - goerrors "errors" -) - -// ParameterSetId represents the security level parameters defined in: -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-parameters-and-arti -type ParameterSetId uint8 -const ( - Param128s ParameterSetId = 1 - Param128f ParameterSetId = 2 - Param192s ParameterSetId = 3 - Param192f ParameterSetId = 4 - Param256s ParameterSetId = 5 - Param256f ParameterSetId = 6 -) - -// ParseParameterSetID parses the ParameterSetId from a byte, returning an error if it's not recognised -func ParseParameterSetID(data [1]byte) (setId ParameterSetId, err error) { - setId = ParameterSetId(data[0]) - switch setId { - case Param128s, Param128f, Param192s, Param192f, Param256s, Param256f: - return setId, nil - default: - return 0, goerrors.New("packet: unsupported SLH-DSA parameter id") - } -} - -// GetPkLen returns the size of the public key in octets according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms -func (setId ParameterSetId) GetPkLen() int { - switch setId { - case Param128s, Param128f: - return 32 - case Param192s, Param192f: - return 48 - case Param256s, Param256f: - return 64 - default: - panic("slhdsa: unsupported parameter") - } -} - -// GetSkLen returns the size of the secret key in octets according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms -func (setId ParameterSetId) GetSkLen() int { - switch setId { - case Param128s, Param128f: - return 64 - case Param192s, Param192f: - return 96 - case Param256s, Param256f: - return 128 - default: - panic("slhdsa: unsupported parameter") - } -} - -// GetSigLen returns the size of the signature in octets according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-the-slh-dsa-algorithms -func (setId ParameterSetId) GetSigLen() int { - switch setId { - case Param128s: - return 7856 - case Param128f: - return 17088 - case Param192s: - return 16224 - case Param192f: - return 35664 - case Param256s: - return 29792 - case Param256f: - return 49856 - default: - panic("slhdsa: unsupported parameter") - } -} - -func (setId ParameterSetId) EncodedBytes() []byte { - return []byte{byte(setId)} -} diff --git a/openpgp/slhdsa/sphincs.go b/openpgp/slhdsa/sphincs.go deleted file mode 100644 index bf9a8be7..00000000 --- a/openpgp/slhdsa/sphincs.go +++ /dev/null @@ -1,155 +0,0 @@ -// Package slhdsa implements SLH-DSA suitable for OpenPGP, experimental. -// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-slh-dsa-2 -package slhdsa - -import ( - "crypto/subtle" - goerrors "errors" - "io" - - "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/kasperdi/SPHINCSPLUS-golang/parameters" - "github.com/kasperdi/SPHINCSPLUS-golang/sphincs" -) - -// Mode defines the underlying hash and mode depending on the algorithm ID as specified here: -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-parameter-specification -type Mode uint8 -const ( - ModeSimpleSHA2 Mode = 1 - ModeSimpleShake Mode = 2 -) - -type PublicKey struct { - ParameterSetId ParameterSetId - Mode Mode - Parameters *parameters.Parameters - PublicData *sphincs.SPHINCS_PK -} - -type PrivateKey struct { - PublicKey - SecretData *sphincs.SPHINCS_SK -} - -func (priv *PrivateKey) SerializePrivate ()([]byte, error) { - return priv.SecretData.SerializeSK() -} - -func (priv *PrivateKey) UnmarshalPrivate (data []byte) (err error) { - // Copy data to prevent library from using an older reference - serialized := make([]byte, len(data)) - copy(serialized, data) - - priv.SecretData, err = sphincs.DeserializeSK(priv.Parameters, serialized) - if err != nil { - return err - } - - return nil -} - -func (pub *PublicKey) SerializePublic ()([]byte, error) { - return pub.PublicData.SerializePK() -} - -func (pub *PublicKey) UnmarshalPublic (data []byte) (err error) { - // Copy data to prevent library from using an older reference - serialized := make([]byte, len(data)) - copy(serialized, data) - - pub.PublicData, err = sphincs.DeserializePK(pub.Parameters, serialized) - if err != nil { - return err - } - - return nil -} - -// GenerateKey generates a SLH-DSA key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation -func GenerateKey(_ io.Reader, mode Mode, param ParameterSetId) (priv *PrivateKey, err error) { - priv = new(PrivateKey) - - priv.ParameterSetId = param - priv.Mode = mode - if priv.Parameters, err = GetParametersFromModeAndId(mode, param); err != nil { - return nil, err - } - - // TODO: add error handling to library - // TODO: accept external randomness source - priv.SecretData, priv.PublicData = sphincs.Spx_keygen(priv.Parameters) - - return -} - -// Sign generates a SLH-DSA signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-generation-2 -func Sign(priv *PrivateKey, message []byte) ([]byte, error) { - sig := sphincs.Spx_sign(priv.Parameters, message, priv.SecretData) - return sig.SerializeSignature() -} - -// Verify verifies a SLH-DSA signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-verification-2 -func Verify(pub *PublicKey, message, sig []byte) bool { - deserializedSig, err := sphincs.DeserializeSignature(pub.Parameters, sig) - if err != nil { - return false - } - - return sphincs.Spx_verify(pub.Parameters, message, deserializedSig, pub.PublicData) -} - -// Validate checks that the public key corresponds to the private key -func Validate(priv *PrivateKey) (err error) { - if subtle.ConstantTimeCompare(priv.PublicData.PKseed, priv.SecretData.PKseed) == 0 || - subtle.ConstantTimeCompare(priv.PublicData.PKroot, priv.SecretData.PKroot) == 0 { - return errors.KeyInvalidError("slhdsa: invalid public key") - } - - return -} - -// GetParametersFromModeAndId returns the instance Parameters given a Mode and a ParameterSetID -func GetParametersFromModeAndId(mode Mode, param ParameterSetId) (*parameters.Parameters, error) { - switch mode { - case ModeSimpleSHA2: - switch param { - case Param128s: - return parameters.MakeSphincsPlusSHA256128sSimple(false), nil - case Param128f: - return parameters.MakeSphincsPlusSHA256128fSimple(false), nil - case Param192s: - return parameters.MakeSphincsPlusSHA256192sSimple(false), nil - case Param192f: - return parameters.MakeSphincsPlusSHA256192fSimple(false), nil - case Param256s: - return parameters.MakeSphincsPlusSHA256256sSimple(false), nil - case Param256f: - return parameters.MakeSphincsPlusSHA256256fSimple(false), nil - default: - return nil, goerrors.New("slhdsa: invalid sha2 parameter") - } - case ModeSimpleShake: - switch param { - case Param128s: - return parameters.MakeSphincsPlusSHAKE256128sSimple(false), nil - case Param128f: - return parameters.MakeSphincsPlusSHAKE256128fSimple(false), nil - case Param192s: - return parameters.MakeSphincsPlusSHAKE256192sSimple(false), nil - case Param192f: - return parameters.MakeSphincsPlusSHAKE256192fSimple(false), nil - case Param256s: - return parameters.MakeSphincsPlusSHAKE256256sSimple(false), nil - case Param256f: - return parameters.MakeSphincsPlusSHAKE256256fSimple(false), nil - default: - return nil, goerrors.New("slhdsa: invalid shake parameter") - } - default: - return nil, goerrors.New("slhdsa: invalid hash algorithm") - } -} diff --git a/openpgp/slhdsa/sphincs_test.go b/openpgp/slhdsa/sphincs_test.go deleted file mode 100644 index e02b3ff0..00000000 --- a/openpgp/slhdsa/sphincs_test.go +++ /dev/null @@ -1,124 +0,0 @@ -// Package slh_dsa_test tests the implementation of SLH-DSA signatures, suitable for OpenPGP, experimental. -package slhdsa_test - -import ( - "crypto/rand" - "io" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp/slhdsa" -) - -func TestSignVerify(t *testing.T) { - asymmAlgos := map[string] slhdsa.Mode{ - "SHA2-Simple": slhdsa.ModeSimpleSHA2, - "SHAKE-Simple": slhdsa.ModeSimpleShake, - } - - params := map[string] slhdsa.ParameterSetId { - "1": slhdsa.Param128s, - "2": slhdsa.Param128f, - "3": slhdsa.Param192s, - "4": slhdsa.Param192f, - "5": slhdsa.Param256s, - "6": slhdsa.Param256f, - } - - for asymmName, asymmAlgo := range asymmAlgos { - t.Run(asymmName, func(t *testing.T) { - for paramName, param := range params { - t.Run(paramName, func(t *testing.T) { - key := testGenerateKeyAlgo(t, asymmAlgo, param) - testSignVerifyAlgo(t, key) - testvalidateAlgo(t, asymmAlgo, param) - }) - } - }) - } -} - -func testvalidateAlgo(t *testing.T, mode slhdsa.Mode, param slhdsa.ParameterSetId) { - key := testGenerateKeyAlgo(t, mode, param) - if err := slhdsa.Validate(key); err != nil { - t.Fatalf("valid key marked as invalid: %s", err) - } - - // Serialize - pkBin, err := key.SerializePublic() - if err != nil { - t.Fatalf("unable to serialize public key") - } - - skBin, err := key.SerializePrivate() - if err != nil { - t.Fatalf("unable to serialize private key") - } - - // Deserialize - if err = key.UnmarshalPublic(pkBin); err != nil { - t.Fatalf("unable to deserialize public key") - } - - if err = key.UnmarshalPrivate(skBin); err != nil { - t.Fatalf("unable to deserialize private key") - } - - if err := slhdsa.Validate(key); err != nil { - t.Fatalf("valid key marked as invalid: %s", err) - } - - // Corrupt the root of the public key - key.PublicData.PKroot[1] ^= 1 - - if err := slhdsa.Validate(key); err == nil { - t.Fatalf("failed to detect invalid root in key") - } - - // Re-load the correct public key - if err = key.UnmarshalPublic(pkBin); err != nil { - t.Fatalf("unable to deserialize public key") - } - - if err = key.UnmarshalPrivate(skBin); err != nil { - t.Fatalf("unable to deserialize private key") - } - - if err := slhdsa.Validate(key); err != nil { - t.Fatalf("valid key marked as invalid: %s", err) - } - - // Corrupt the seed of the public key - key.PublicData.PKseed[1] ^= 1 - - if err := slhdsa.Validate(key); err == nil { - t.Fatalf("failed to detect invalid seed in key") - } -} - -func testGenerateKeyAlgo(t *testing.T, mode slhdsa.Mode, param slhdsa.ParameterSetId) *slhdsa.PrivateKey { - priv, err := slhdsa.GenerateKey(rand.Reader, mode, param) - 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) - } - - sig, err := slhdsa.Sign(priv, digest) - if err != nil { - t.Errorf("error encrypting: %s", err) - } - - result := slhdsa.Verify(&priv.PublicKey, digest, sig) - if !result { - t.Error("unable to verify message") - } -} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index b7d52d11..ed647656 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -25,7 +25,6 @@ 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" @@ -426,18 +425,6 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { } return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) - case packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: - if !config.V6() { - return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSA key") - } - - mode, err := packet.GetSlhdsaModeFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - parameter := config.SlhdsaParam() - - return slhdsa.GenerateKey(config.Random(), mode, parameter) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -486,7 +473,7 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384, packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + packet.PubKeyAlgoMldsa87Brainpool384: if pubKeyAlgo, err = packet.GetMatchingMlkemKem(config.PublicKeyAlgorithm()); err != nil { return nil, err } diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go index e60ef427..1d113255 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -215,8 +215,7 @@ func (s *Subkey) IsPQ() bool { case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384, - packet.PubKeyAlgoSlhdsaSha2, packet.PubKeyAlgoSlhdsaShake: + packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384: return true default: return false From f8daf26e0f7ef90f3a6d4194ecdcb4fa8dcc275e Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 12:08:01 +0200 Subject: [PATCH 23/36] fix: Remove fmt.Println statements --- openpgp/mlkem_ecdh/mlkem_ecdh.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index 04556181..dc61a4dd 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -4,10 +4,10 @@ package mlkem_ecdh import ( goerrors "errors" - "fmt" + "io" + "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "golang.org/x/crypto/sha3" - "io" "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -16,9 +16,9 @@ import ( ) type PublicKey struct { - AlgId uint8 - Curve ecc.ECDHCurve - Mlkem kem.Scheme + AlgId uint8 + Curve ecc.ECDHCurve + Mlkem kem.Scheme PublicMlkem kem.PublicKey PublicPoint []byte } @@ -60,7 +60,7 @@ func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemera return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") } - if len(msg) % 8 != 0 { + if len(msg)%8 != 0 { return nil, nil, nil, goerrors.New("mlkem_ecdh: session key not a multiple of 8") } @@ -114,11 +114,7 @@ func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg return nil, err } - msg, err = keywrap.Unwrap(kek, ciphertext) - - fmt.Printf("kek:%x\nsk:%x\n", kek, msg) - - return msg, err + return keywrap.Unwrap(kek, ciphertext) } // buildKey implements the composite KDF as specified in @@ -153,8 +149,6 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK _, _ = k.Write([]byte{pub.AlgId}) _, _ = k.Write([]byte("OpenPGPCompositeKDFv1")) - fmt.Printf("ecc:%x\nkyber:%x\n", eccKeyShare, mlkemKeyShare) - return k.Sum(nil), nil } @@ -239,4 +233,4 @@ func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, en } return -} \ No newline at end of file +} From b68ddfbf1c25d762f91c4c47979af174ab1b5bf0 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 12:10:04 +0200 Subject: [PATCH 24/36] chore: Run go fmt on openpgp folder --- openpgp/ecdh/ecdh_test.go | 2 +- .../ecc/curve25519/curve25519_test.go | 8 ++++---- .../ecc/curve25519/field/fe_alias_test.go | 4 ++-- .../internal/ecc/curve25519/field/fe_amd64.go | 3 +++ openpgp/internal/ecc/generic.go | 2 +- openpgp/internal/encoding/octetarray.go | 6 +++--- .../encoding/short_byte_string_test.go | 8 ++++---- openpgp/keys_test.go | 6 +++--- openpgp/mldsa_ecdsa/mldsa_ecdsa.go | 12 +++++------ openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go | 7 +++---- openpgp/mldsa_eddsa/mldsa_eddsa.go | 6 +++--- openpgp/mldsa_eddsa/mldsa_eddsa_test.go | 5 ++--- openpgp/mlkem_ecdh/mlkem_ecdh_test.go | 20 +++++++++---------- openpgp/packet/forwarding.go | 4 ++-- openpgp/pqc_vectors_test.go | 11 +++++----- openpgp/read_test.go | 18 ++++++++--------- openpgp/s2k/s2k.go | 4 ++-- openpgp/symmetric/aead.go | 11 +++++----- openpgp/symmetric/hmac.go | 8 ++++---- openpgp/write_test.go | 2 +- 20 files changed, 72 insertions(+), 75 deletions(-) diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 0e79778f..0170d776 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -42,7 +42,7 @@ func TestCurves(t *testing.T) { } func testGenerate(t *testing.T, curve ecc.ECDHCurve) *PrivateKey { - kdf := KDF { + kdf := KDF{ Hash: algorithm.SHA512, Cipher: algorithm.AES256, } diff --git a/openpgp/internal/ecc/curve25519/curve25519_test.go b/openpgp/internal/ecc/curve25519/curve25519_test.go index 88921267..bd82e03e 100644 --- a/openpgp/internal/ecc/curve25519/curve25519_test.go +++ b/openpgp/internal/ecc/curve25519/curve25519_test.go @@ -8,12 +8,12 @@ import ( ) const ( - hexBobSecret = "5989216365053dcf9e35a04b2a1fc19b83328426be6bb7d0a2ae78105e2e3188" - hexCharlesSecret = "684da6225bcd44d880168fc5bec7d2f746217f014c8019005f144cc148f16a00" + hexBobSecret = "5989216365053dcf9e35a04b2a1fc19b83328426be6bb7d0a2ae78105e2e3188" + hexCharlesSecret = "684da6225bcd44d880168fc5bec7d2f746217f014c8019005f144cc148f16a00" hexExpectedProxyParam = "e89786987c3a3ec761a679bc372cd11a425eda72bd5265d78ad0f5f32ee64f02" - hexMessagePoint = "aaea7b3bb92f5f545d023ccb15b50f84ba1bdd53be7f5cfadcfb0106859bf77e" - hexInputProxyParam = "83c57cbe645a132477af55d5020281305860201608e81a1de43ff83f245fb302" + hexMessagePoint = "aaea7b3bb92f5f545d023ccb15b50f84ba1bdd53be7f5cfadcfb0106859bf77e" + hexInputProxyParam = "83c57cbe645a132477af55d5020281305860201608e81a1de43ff83f245fb302" hexExpectedTransformedPoint = "ec31bb937d7ef08c451d516be1d7976179aa7171eea598370661d1152b85005a" hexSmallSubgroupPoint = "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" diff --git a/openpgp/internal/ecc/curve25519/field/fe_alias_test.go b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go index 5ad81df0..64e57c4f 100644 --- a/openpgp/internal/ecc/curve25519/field/fe_alias_test.go +++ b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go @@ -77,11 +77,11 @@ func checkAliasingTwoArgs(f func(v, x, y *Element) *Element) func(v, x, y Elemen // 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) +// v.Invert(v) // // or // -// v.Add(v, v) +// v.Add(v, v) // // without any of the inputs getting clobbered by the output being written. func TestAliasing(t *testing.T) { diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.go b/openpgp/internal/ecc/curve25519/field/fe_amd64.go index 44dc8e8c..edcf163c 100644 --- a/openpgp/internal/ecc/curve25519/field/fe_amd64.go +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.go @@ -1,13 +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/generic.go b/openpgp/internal/ecc/generic.go index e50c532f..44fad3b4 100644 --- a/openpgp/internal/ecc/generic.go +++ b/openpgp/internal/ecc/generic.go @@ -57,7 +57,7 @@ func (c *genericCurve) UnmarshalIntegerSecret(d []byte) *big.Int { } func (c *genericCurve) MarshalFieldInteger(i *big.Int) (b []byte) { - b = make([]byte, (c.Curve.Params().BitSize + 7) / 8) + b = make([]byte, (c.Curve.Params().BitSize+7)/8) return i.FillBytes(b) } diff --git a/openpgp/internal/encoding/octetarray.go b/openpgp/internal/encoding/octetarray.go index ac6d6bb1..e5e4a827 100644 --- a/openpgp/internal/encoding/octetarray.go +++ b/openpgp/internal/encoding/octetarray.go @@ -11,21 +11,21 @@ import ( // OctetArray is used to store a fixed-length field type OctetArray struct { length int - bytes []byte + bytes []byte } // NewOctetArray returns a OID initialized with bytes. func NewOctetArray(bytes []byte) *OctetArray { return &OctetArray{ length: len(bytes), - bytes: bytes, + bytes: bytes, } } func NewEmptyOctetArray(length int) *OctetArray { return &OctetArray{ length: length, - bytes: nil, + bytes: nil, } } diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go index 6544b4ec..37510a35 100644 --- a/openpgp/internal/encoding/short_byte_string_test.go +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -1,18 +1,18 @@ package encoding import ( - "testing" "bytes" + "testing" ) var octetStreamTests = []struct { data []byte -} { +}{ { data: []byte{0x0, 0x0, 0x0}, }, { - data: []byte {0x1, 0x2, 0x03}, + data: []byte{0x1, 0x2, 0x03}, }, { data: make([]byte, 255), @@ -56,6 +56,6 @@ func TestShortByteString(t *testing.T) { } } -func checkEquality (left *ShortByteString, right *ShortByteString) bool { +func checkEquality(left *ShortByteString, right *ShortByteString) bool { return (left.length == right.length) && (bytes.Equal(left.data, right.data)) } diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 1734cd24..ce602f36 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2087,9 +2087,9 @@ TxGVotQ4A/0u0VbOMEUfnrI8Fms= } func TestAddV4MlkemSubkey(t *testing.T) { eddsaConfig := &packet.Config{ - DefaultHash: crypto.SHA512, - Algorithm: packet.PubKeyAlgoEdDSA, - V6Keys: false, + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEdDSA, + V6Keys: false, DefaultCipher: packet.CipherAES256, Time: func() time.Time { parsed, _ := time.Parse("2006-01-02", "2013-07-01") diff --git a/openpgp/mldsa_ecdsa/mldsa_ecdsa.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa.go index c5cfd92f..31a448ac 100644 --- a/openpgp/mldsa_ecdsa/mldsa_ecdsa.go +++ b/openpgp/mldsa_ecdsa/mldsa_ecdsa.go @@ -14,16 +14,16 @@ import ( ) type PublicKey struct { - AlgId uint8 - Curve ecc.ECDSACurve - Mldsa dilithium.Mode - X, Y *big.Int + AlgId uint8 + Curve ecc.ECDSACurve + Mldsa dilithium.Mode + X, Y *big.Int PublicMldsa dilithium.PublicKey } type PrivateKey struct { PublicKey - SecretEc *big.Int + SecretEc *big.Int SecretMldsa dilithium.PrivateKey } @@ -115,4 +115,4 @@ func Validate(priv *PrivateKey) (err error) { } return -} \ No newline at end of file +} diff --git a/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go index 3dae1f49..f11efbff 100644 --- a/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go +++ b/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go @@ -12,9 +12,9 @@ import ( ) func TestSignVerify(t *testing.T) { - asymmAlgos := map[string] packet.PublicKeyAlgorithm { - "ML-DSA3_P256": packet.PubKeyAlgoMldsa65p256, - "ML-DSA5_P384": packet.PubKeyAlgoMldsa87p384, + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA3_P256": packet.PubKeyAlgoMldsa65p256, + "ML-DSA5_P384": packet.PubKeyAlgoMldsa87p384, "ML-DSA3_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, "ML-DSA5_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, } @@ -73,7 +73,6 @@ func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_e return priv } - func testSignVerifyAlgo(t *testing.T, priv *mldsa_ecdsa.PrivateKey) { digest := make([]byte, 32) _, err := io.ReadFull(rand.Reader, digest[:]) diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go index 812a51c3..d64f2718 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -13,7 +13,7 @@ import ( ) type PublicKey struct { - AlgId uint8 + AlgId uint8 Curve ecc.EdDSACurve Mldsa dilithium.Mode PublicPoint []byte @@ -22,7 +22,7 @@ type PublicKey struct { type PrivateKey struct { PublicKey - SecretEc []byte + SecretEc []byte SecretMldsa dilithium.PrivateKey } @@ -82,4 +82,4 @@ func Validate(priv *PrivateKey) (err error) { return errors.KeyInvalidError("mldsa_eddsa: invalid public key") } return -} \ No newline at end of file +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go index b35b8275..b848c330 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -11,9 +11,9 @@ import ( ) func TestSignVerify(t *testing.T) { - asymmAlgos := map[string] packet.PublicKeyAlgorithm { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ "ML-DSA3_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, - "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, + "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, } for asymmName, asymmAlgo := range asymmAlgos { @@ -70,7 +70,6 @@ func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_e return priv } - func testSignVerifyAlgo(t *testing.T, priv *mldsa_eddsa.PrivateKey) { digest := make([]byte, 32) _, err := io.ReadFull(rand.Reader, digest[:]) diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go index fb0a0b9a..e25f0430 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -11,16 +11,16 @@ import ( ) func TestEncryptDecrypt(t *testing.T) { - asymmAlgos := map[string] packet.PublicKeyAlgorithm { - "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, - "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, - "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, - "Mlkem1024_P384":packet.PubKeyAlgoMlkem1024P384, - "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, - "Mlkem1024_Brainpool384":packet.PubKeyAlgoMlkem1024Brainpool384, + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, + "Mlkem1024_P384": packet.PubKeyAlgoMlkem1024P384, + "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, + "Mlkem1024_Brainpool384": packet.PubKeyAlgoMlkem1024Brainpool384, } - symmAlgos := map[string] algorithm.Cipher { + symmAlgos := map[string]algorithm.Cipher{ "AES-128": algorithm.AES128, "AES-192": algorithm.AES192, "AES-256": algorithm.AES256, @@ -62,7 +62,7 @@ func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { 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") @@ -84,7 +84,7 @@ func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_e if err != nil { t.Fatal(err) } - + return priv } diff --git a/openpgp/packet/forwarding.go b/openpgp/packet/forwarding.go index 50b4de44..f16a2fbd 100644 --- a/openpgp/packet/forwarding.go +++ b/openpgp/packet/forwarding.go @@ -4,7 +4,7 @@ import "encoding/binary" // ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) type ForwardingInstance struct { - KeyVersion int + KeyVersion int ForwarderFingerprint []byte ForwardeeFingerprint []byte ProxyParameter []byte @@ -33,4 +33,4 @@ func computeForwardingKeyId(fingerprint []byte, version int) uint64 { default: panic("invalid pgp key version") } -} \ No newline at end of file +} diff --git a/openpgp/pqc_vectors_test.go b/openpgp/pqc_vectors_test.go index 5c242076..da6451eb 100644 --- a/openpgp/pqc_vectors_test.go +++ b/openpgp/pqc_vectors_test.go @@ -61,7 +61,7 @@ func encryptPqcMessageVector(t *testing.T, filename string, entity *Entity, conf t.Fatalf("Failed to init armoring: %s", err) } - w, err := Encrypt(serializedMessage, []*Entity{entity},nil, nil /* no hints */, config) + w, err := Encrypt(serializedMessage, []*Entity{entity}, nil, nil /* no hints */, config) if err != nil { t.Fatalf("Error in Encrypt: %s", err) } @@ -138,10 +138,10 @@ func TestV4EddsaPqKey(t *testing.T) { var configV1 = &packet.Config{ DefaultCipher: packet.CipherAES256, - AEADConfig: nil, + AEADConfig: nil, } - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1,true) + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1, true) var configV2 = &packet.Config{ DefaultCipher: packet.CipherAES256, @@ -150,10 +150,9 @@ func TestV4EddsaPqKey(t *testing.T) { }, } - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2,false) + encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2, false) } - func TestV6EddsaPqKey(t *testing.T) { //eddsaConfig := &packet.Config{ // DefaultHash: crypto.SHA512, @@ -213,5 +212,5 @@ func TestV6EddsaPqKey(t *testing.T) { }, } - encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2,false) + encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) } diff --git a/openpgp/read_test.go b/openpgp/read_test.go index bbc6c7cc..50d9c3c4 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -941,13 +941,12 @@ func TestReadV5Messages(t *testing.T) { } } - var pqcDraftVectors = map[string]struct { armoredPrivateKey string - armoredPublicKey string - fingerprints []string - armoredMessages []string - v6 bool + armoredPublicKey string + fingerprints []string + armoredMessages []string + v6 bool }{ "v4_Ed25519_ML-KEM-768+X25519": { v4Ed25519Mlkem768X25519PrivateTestVector, @@ -955,7 +954,6 @@ var pqcDraftVectors = map[string]struct { []string{"b2e9b532d55bd6287ec79e17c62adc0ddd1edd73", "95bed3c63f295e7b980b6a2b93b3233faf28c9d2", "bd67d98388813e88bf3490f3e440cfbaffd6f357"}, []string{v4Ed25519Mlkem768X25519PrivateV1MessageTestVector, v4Ed25519Mlkem768X25519PrivateV2MessageTestVector}, false, - }, "v6_Ed25519_ML-KEM-768+X25519": { v6Ed25519Mlkem768X25519PrivateTestVector, @@ -979,8 +977,8 @@ func TestPqcDraftVectors(t *testing.T) { 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 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] { @@ -988,7 +986,7 @@ func TestPqcDraftVectors(t *testing.T) { } for i, subkey := range secretKey[0].Subkeys { - if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1]{ + 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) } } @@ -1012,7 +1010,7 @@ func TestPqcDraftVectors(t *testing.T) { } for i, armoredMessage := range test.armoredMessages { - t.Run("Decrypt_message_" + strconv.Itoa(i), func(t *testing.T) { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { msgReader, err := armor.Decode(strings.NewReader(armoredMessage)) if err != nil { t.Error(err) diff --git a/openpgp/s2k/s2k.go b/openpgp/s2k/s2k.go index 92511580..c9f6a46a 100644 --- a/openpgp/s2k/s2k.go +++ b/openpgp/s2k/s2k.go @@ -199,8 +199,8 @@ func Generate(rand io.Reader, c *Config) (*Params, error) { } params = &Params{ - mode: SaltedS2K, - hashId: hashId, + mode: SaltedS2K, + hashId: hashId, } } else { // Enforce IteratedSaltedS2K method otherwise hashId, ok := algorithm.HashToHashId(c.hash()) diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go index 044b1394..b9d389dc 100644 --- a/openpgp/symmetric/aead.go +++ b/openpgp/symmetric/aead.go @@ -6,15 +6,15 @@ import ( ) type AEADPublicKey struct { - Cipher algorithm.CipherFunction + Cipher algorithm.CipherFunction BindingHash [32]byte - Key []byte + Key []byte } type AEADPrivateKey struct { PublicKey AEADPublicKey - HashSeed [32]byte - Key []byte + HashSeed [32]byte + Key []byte } func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { @@ -29,7 +29,7 @@ func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEA func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { priv = new(AEADPrivateKey) - var seed [32] byte + var seed [32]byte _, err = rand.Read(seed[:]) if err != nil { return @@ -73,4 +73,3 @@ func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algori message, err = aead.Open(nil, nonce, ciphertext, nil) return } - diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go index fd4a7cbb..e9d61475 100644 --- a/openpgp/symmetric/hmac.go +++ b/openpgp/symmetric/hmac.go @@ -11,7 +11,7 @@ import ( ) type HMACPublicKey struct { - Hash algorithm.Hash + 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 @@ -23,8 +23,8 @@ type HMACPublicKey struct { type HMACPrivateKey struct { PublicKey HMACPublicKey - HashSeed [32]byte - Key []byte + HashSeed [32]byte + Key []byte } func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { @@ -39,7 +39,7 @@ func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { priv = new(HMACPrivateKey) - var seed [32] byte + var seed [32]byte _, err = rand.Read(seed[:]) if err != nil { return diff --git a/openpgp/write_test.go b/openpgp/write_test.go index 1ac9e131..421d79e8 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -522,7 +522,7 @@ func TestSymmetricEncryptionSEIPDv2RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = map[string] struct { +var testEncryptionTests = map[string]struct { keyRingHex string isSigned bool okV6 bool From bce1652b392f1a98272cd79b599e77e542e2cd20 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 14:06:11 +0200 Subject: [PATCH 25/36] Remove PQC algorithms with brainpool and nist curves --- openpgp/benchmark_v6_test.go | 12 - openpgp/key_generation.go | 24 +- openpgp/keys.go | 6 +- openpgp/keys_test.go | 8 +- openpgp/keys_test_data.go | 391 ------------------------ openpgp/keys_v6_test.go | 16 +- openpgp/mldsa_ecdsa/mldsa_ecdsa.go | 118 ------- openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go | 92 ------ openpgp/mlkem_ecdh/mlkem_ecdh_test.go | 11 +- openpgp/packet/encrypted_key.go | 42 +-- openpgp/packet/packet.go | 22 +- openpgp/packet/private_key.go | 58 +--- openpgp/packet/public_key.go | 125 +------- openpgp/packet/signature.go | 57 +--- openpgp/pqc_vectors_test.go | 80 +---- openpgp/read.go | 3 +- openpgp/v2/key_generation.go | 24 +- openpgp/v2/read.go | 3 +- openpgp/v2/subkeys.go | 6 +- openpgp/write_test.go | 4 +- 20 files changed, 48 insertions(+), 1054 deletions(-) delete mode 100644 openpgp/mldsa_ecdsa/mldsa_ecdsa.go delete mode 100644 openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go index e40208f1..c0593765 100644 --- a/openpgp/benchmark_v6_test.go +++ b/openpgp/benchmark_v6_test.go @@ -65,18 +65,6 @@ var benchmarkTestSet = map[string]*packet.Config{ "ML-DSA5Ed448_ML-KEM1024X448": { Algorithm: packet.PubKeyAlgoMldsa87Ed448, }, - "ML-DSA3P256_ML-KEM768P256": { - Algorithm: packet.PubKeyAlgoMldsa65p256, - }, - "ML-DSA5P384_ML-KEM1024P384": { - Algorithm: packet.PubKeyAlgoMldsa87p384, - }, - "ML-DSA3Brainpool256_ML-KEM768Brainpool256": { - Algorithm: packet.PubKeyAlgoMldsa65Brainpool256, - }, - "ML-DSA5Brainpool384_ML-KEM1024Brainpool384": { - Algorithm: packet.PubKeyAlgoMldsa87Brainpool384, - }, } func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index e033cc9c..a60b8b68 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -13,7 +13,6 @@ import ( "math/big" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/ecdh" @@ -316,22 +315,6 @@ 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.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384: - if !config.V6() { - return nil, goerrors.New("openpgp: cannot create a non-v6 ML-DSA + ECDSA key") - } - - c, err := packet.GetEcdsaCurveFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - - return mldsa_ecdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if !config.V6() { return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") @@ -393,15 +376,12 @@ 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, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkemKem(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, packet.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) if err != nil { diff --git a/openpgp/keys.go b/openpgp/keys.go index 6b1da154..62a60b77 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -313,10 +313,8 @@ func (s *Subkey) Revoked(now time.Time) bool { // 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.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, - packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384: + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: return true default: return false diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index ce602f36..09f50dc5 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2109,12 +2109,8 @@ 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, - "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, - "Mlkem1024_P384": packet.PubKeyAlgoMlkem1024P384, - "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, - "Mlkem1024_Brainpool384": packet.PubKeyAlgoMlkem1024Brainpool384, + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, } for name, algo := range asymmAlgos { diff --git a/openpgp/keys_test_data.go b/openpgp/keys_test_data.go index 82481ac9..108fd096 100644 --- a/openpgp/keys_test_data.go +++ b/openpgp/keys_test_data.go @@ -536,394 +536,3 @@ VppQxdtxPvAA/34snHBX7Twnip1nMt7P4e2hDiw/hwQ7oqioOvc6jMkP =Z8YJ -----END PGP PRIVATE KEY BLOCK----- ` -const eddsaMlkem512X25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- - -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 -ByIM -=camV ------END PGP PRIVATE KEY BLOCK-----` - -const eddsaMlkem1024X448PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- - -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 -R/YH -=MhqF ------END PGP PRIVATE KEY BLOCK-----` - -const eddsaMlkem768P384PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- - -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 -=mDQF ------END PGP PRIVATE KEY BLOCK-----` - -const eddsaMlkem1024P521PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- - -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 -=3UOU ------END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index 2f218982..0e74d44a 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" @@ -212,12 +211,8 @@ func TestGeneratePqKey(t *testing.T) { } asymmAlgos := map[string]packet.PublicKeyAlgorithm{ - "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, - "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, - "ML-DSA65_P256": packet.PubKeyAlgoMldsa65p256, - "ML-DSA87_P384": packet.PubKeyAlgoMldsa87p384, - "ML-DSA65_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, - "ML-DSA87_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, + "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, } for name, algo := range asymmAlgos { @@ -268,13 +263,6 @@ func TestGeneratePqKey(t *testing.T) { t.Fatal(err) } - // Corrupt public ML-DSA in primary key - if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_ecdsa.PublicKey); ok { - bin := pk.PublicMldsa.Bytes() - bin[5] ^= 1 - pk.PublicMldsa = pk.Mldsa.PublicKeyFromBytes(bin) - } - if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { bin := pk.PublicMldsa.Bytes() bin[5] ^= 1 diff --git a/openpgp/mldsa_ecdsa/mldsa_ecdsa.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa.go deleted file mode 100644 index 31a448ac..00000000 --- a/openpgp/mldsa_ecdsa/mldsa_ecdsa.go +++ /dev/null @@ -1,118 +0,0 @@ -// Package mldsa_ecdsa implements hybrid ML-DSA + ECDSA encryption, suitable for OpenPGP, experimental. -// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-signature-schemes -package mldsa_ecdsa - -import ( - "crypto/subtle" - goerrors "errors" - "github.com/cloudflare/circl/sign/dilithium" - "io" - "math/big" - - "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" -) - -type PublicKey struct { - AlgId uint8 - Curve ecc.ECDSACurve - Mldsa dilithium.Mode - X, Y *big.Int - PublicMldsa dilithium.PublicKey -} - -type PrivateKey struct { - PublicKey - SecretEc *big.Int - SecretMldsa dilithium.PrivateKey -} - -func (pk *PublicKey) MarshalPoint() []byte { - return pk.Curve.MarshalIntegerPoint(pk.X, pk.Y) -} - -func (pk *PublicKey) UnmarshalPoint(p []byte) error { - pk.X, pk.Y = pk.Curve.UnmarshalIntegerPoint(p) - if pk.X == nil { - return goerrors.New("mldsa_ecdsa: failed to parse EC point") - } - return nil -} - -func (sk *PrivateKey) MarshalIntegerSecret() []byte { - return sk.Curve.MarshalFieldInteger(sk.SecretEc) -} - -func (sk *PrivateKey) UnmarshalIntegerSecret(d []byte) error { - sk.SecretEc = sk.Curve.UnmarshalFieldInteger(d) - - if sk.SecretEc == nil { - return goerrors.New("mldsa_ecdsa: failed to parse scalar") - } - return nil -} - -// GenerateKey generates a ML-DSA + ECDSA composite key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation-procedure-2 -func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDSACurve, d dilithium.Mode) (priv *PrivateKey, err error) { - priv = new(PrivateKey) - - priv.PublicKey.AlgId = algId - priv.PublicKey.Curve = c - priv.PublicKey.Mldsa = d - - priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc, err = c.GenerateECDSA(rand) - if err != nil { - return nil, err - } - - priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey(rand) - return -} - -// Sign generates a ML-DSA + ECDSA composite signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-generation -func Sign(rand io.Reader, priv *PrivateKey, message []byte) (dSig, ecR, ecS []byte, err error) { - r, s, err := priv.PublicKey.Curve.Sign(rand, priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc, message) - if err != nil { - return nil, nil, nil, err - } - - ecR = priv.PublicKey.Curve.MarshalFieldInteger(r) - ecS = priv.PublicKey.Curve.MarshalFieldInteger(s) - - dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message) - if dSig == nil { - return nil, nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") - } - - return -} - -// Verify verifies a ML-DSA + ECDSA composite signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-verification -func Verify(pub *PublicKey, message, dSig, ecR, ecS []byte) bool { - r := pub.Curve.UnmarshalFieldInteger(ecR) - s := pub.Curve.UnmarshalFieldInteger(ecS) - - return pub.Curve.Verify(pub.X, pub.Y, message, r, s) && pub.Mldsa.Verify(pub.PublicMldsa, message, dSig) -} - -// Validate checks that the public key corresponds to the private key -func Validate(priv *PrivateKey) (err error) { - if err = priv.PublicKey.Curve.ValidateECDSA(priv.PublicKey.X, priv.PublicKey.Y, priv.SecretEc.Bytes()); err != nil { - return err - } - - pub := priv.SecretMldsa.Public() - casted, ok := pub.(dilithium.PublicKey) - if !ok { - return errors.KeyInvalidError("mldsa_ecdsa: invalid public key") - } - - if subtle.ConstantTimeCompare(priv.PublicMldsa.Bytes(), casted.Bytes()) == 0 { - return errors.KeyInvalidError("mldsa_ecdsa: invalid public key") - } - - return -} diff --git a/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go b/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go deleted file mode 100644 index f11efbff..00000000 --- a/openpgp/mldsa_ecdsa/mldsa_ecdsa_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Package mldsa_ecdsa_test tests the implementation of hybrid ML-DSA + ECDSA encryption, suitable for OpenPGP, experimental. -package mldsa_ecdsa_test - -import ( - "crypto/rand" - "io" - "math/big" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func TestSignVerify(t *testing.T) { - asymmAlgos := map[string]packet.PublicKeyAlgorithm{ - "ML-DSA3_P256": packet.PubKeyAlgoMldsa65p256, - "ML-DSA5_P384": packet.PubKeyAlgoMldsa87p384, - "ML-DSA3_Brainpool256": packet.PubKeyAlgoMldsa65Brainpool256, - "ML-DSA5_Brainpool384": packet.PubKeyAlgoMldsa87Brainpool384, - } - - 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_ecdsa.Validate(key); err != nil { - t.Fatalf("valid key marked as invalid: %s", err) - } - - bin := key.PublicMldsa.Bytes() - bin[5] ^= 1 - key.PublicMldsa = key.Mldsa.PublicKeyFromBytes(bin) - - if err := mldsa_ecdsa.Validate(key); err == nil { - t.Fatalf("failed to detect invalid key") - } - - // Generate fresh key - key = testGenerateKeyAlgo(t, algId) - if err := mldsa_ecdsa.Validate(key); err != nil { - t.Fatalf("valid key marked as invalid: %s", err) - } - - key.X.Sub(key.X, big.NewInt(1)) - if err := mldsa_ecdsa.Validate(key); err == nil { - t.Fatalf("failed to detect invalid key") - } -} - -func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_ecdsa.PrivateKey { - curveObj, err := packet.GetEcdsaCurveFromAlgID(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_ecdsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) - if err != nil { - t.Fatal(err) - } - - return priv -} - -func testSignVerifyAlgo(t *testing.T, priv *mldsa_ecdsa.PrivateKey) { - digest := make([]byte, 32) - _, err := io.ReadFull(rand.Reader, digest[:]) - if err != nil { - t.Fatal(err) - } - - dSig, ecR, ecS, err := mldsa_ecdsa.Sign(rand.Reader, priv, digest) - if err != nil { - t.Errorf("error encrypting: %s", err) - } - - result := mldsa_ecdsa.Verify(&priv.PublicKey, digest, dSig, ecR, ecS) - if !result { - t.Error("unable to verify message") - } -} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go index e25f0430..23a82eb3 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -4,20 +4,17 @@ 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" - "testing" ) func TestEncryptDecrypt(t *testing.T) { asymmAlgos := map[string]packet.PublicKeyAlgorithm{ - "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, - "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, - "Mlkem768_P256": packet.PubKeyAlgoMlkem768P256, - "Mlkem1024_P384": packet.PubKeyAlgoMlkem1024P384, - "Mlkem768_Brainpool256": packet.PubKeyAlgoMlkem768Brainpool256, - "Mlkem1024_Brainpool384": packet.PubKeyAlgoMlkem1024Brainpool384, + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, } symmAlgos := map[string]algorithm.Cipher{ diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index c60f8df3..0c4bd3a7 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -165,28 +165,10 @@ func (e *EncryptedKey) parse(r io.Reader) (err error) { if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 56, 1568, e.Version == 6); err != nil { return err } - case PubKeyAlgoMlkem768P256: - if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 65, 1088, e.Version == 6); err != nil { - return err - } - case PubKeyAlgoMlkem1024P384: - if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 97, 1568, e.Version == 6); err != nil { - return err - } - case PubKeyAlgoMlkem768Brainpool256: - if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 65, 1088, e.Version == 6); err != nil { - return err - } - case PubKeyAlgoMlkem1024Brainpool384: - if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 97, 1568, e.Version == 6); err != nil { - return err - } } if e.Version < 6 { switch e.Algo { - case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, - PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, - PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: e.CipherFunc = CipherFunction(cipherFunction) // Check for validity is in the Decrypt method } @@ -244,8 +226,7 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: ecE := e.encryptedMPI1.Bytes() kE := e.encryptedMPI2.Bytes() m := e.encryptedMPI3.Bytes() @@ -270,8 +251,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } } key, err = decodeChecksumKey(b[keyOffset:]) - case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, - PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: if e.Version < 6 { switch e.CipherFunc { case CipherAES128, CipherAES192, CipherAES256: @@ -305,8 +285,7 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + int(e.encryptedMPI3.EncodedLength()) + 1 if e.Version < 6 { encodedLength += 1 @@ -381,8 +360,7 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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: @@ -418,9 +396,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph // 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, - PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, - PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519, x448, and PQC") default: break @@ -472,8 +448,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, - PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: // algorithm is added in plaintext below keyBlock = key } @@ -491,8 +466,7 @@ 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, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + 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))) diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 1368ff46..43e79ee2 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -514,20 +514,12 @@ const ( PubKeyAlgoRSASignOnly PublicKeyAlgorithm = 3 // Experimental PQC KEM algorithms - PubKeyAlgoMlkem768X25519 = 105 - PubKeyAlgoMlkem1024X448 = 106 - PubKeyAlgoMlkem768P256 = 31 - PubKeyAlgoMlkem1024P384 = 32 - PubKeyAlgoMlkem768Brainpool256 = 33 - PubKeyAlgoMlkem1024Brainpool384 = 34 + PubKeyAlgoMlkem768X25519 = 105 + PubKeyAlgoMlkem1024X448 = 106 // Experimental PQC DSA algorithms - PubKeyAlgoMldsa65Ed25519 = 107 - PubKeyAlgoMldsa87Ed448 = 108 - PubKeyAlgoMldsa65p256 = 37 - PubKeyAlgoMldsa87p384 = 38 - PubKeyAlgoMldsa65Brainpool256 = 39 - PubKeyAlgoMldsa87Brainpool384 = 40 + PubKeyAlgoMldsa65Ed25519 = 107 + PubKeyAlgoMldsa87Ed448 = 108 ) // CanEncrypt returns true if it's possible to encrypt a message to a public @@ -535,8 +527,7 @@ const ( func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, ExperimentalPubKeyAlgoAEAD, - PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384: + PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return true } return false @@ -547,8 +538,7 @@ func (pka PublicKeyAlgorithm) CanEncrypt() bool { func (pka PublicKeyAlgorithm) CanSign() bool { switch pka { case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, - PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, - PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index b6412660..e0f1a2ba 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -20,7 +20,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" "github.com/cloudflare/circl/kem/mlkem/mlkem768" @@ -177,8 +176,6 @@ 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_ecdsa.PrivateKey: - pk.PublicKey = *NewMldsaEcdsaPublicKey(creationTime, &pubkey.PublicKey) case *mldsa_eddsa.PrivateKey: pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) default: @@ -581,16 +578,6 @@ func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err err return err } -// serializeMldsaEcdsaPrivateKey serializes a ML-DSA + ECDSA private key according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 -func serializeMldsaEcdsaPrivateKey(w io.Writer, priv *mldsa_ecdsa.PrivateKey) error { - if _, err := w.Write(encoding.NewOctetArray(priv.MarshalIntegerSecret()).EncodedBytes()); err != nil { - return err - } - _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsa.Bytes()).EncodedBytes()) - return err -} - // serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to // https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) error { @@ -907,8 +894,6 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeHMACPrivateKey(w, priv) case *mlkem_ecdh.PrivateKey: err = serializeMlkemPrivateKey(w, priv) - case *mldsa_ecdsa.PrivateKey: - err = serializeMldsaEcdsaPrivateKey(w, priv) case *mldsa_eddsa.PrivateKey: err = serializeMldsaEddsaPrivateKey(w, priv) default: @@ -943,20 +928,14 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseAEADPrivateKey(data) case ExperimentalPubKeyAlgoHMAC: return pk.parseHMACPrivateKey(data) - case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: + case PubKeyAlgoMlkem768X25519: return pk.parseMlkemEcdhPrivateKey(data, 32, mlkem768.PrivateKeySize) case PubKeyAlgoMlkem1024X448: return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem1024.PrivateKeySize) - case PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: - return pk.parseMlkemEcdhPrivateKey(data, 48, mlkem1024.PrivateKeySize) case PubKeyAlgoMldsa65Ed25519: return pk.parseMldsaEddsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) case PubKeyAlgoMldsa87Ed448: return pk.parseMldsaEddsaPrivateKey(data, 57, dilithium.MLDSA87.PrivateKeySize()) - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: - return pk.parseMldsaEcdsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) - case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: - return pk.parseMldsaEcdsaPrivateKey(data, 48, dilithium.MLDSA87.PrivateKeySize()) default: err = errors.StructuralError("unknown private key type") return @@ -1280,41 +1259,6 @@ func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { return nil } -// parseMldsaEcdsaPrivateKey parses a ML-DSA + ECDSA private key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 -func (pk *PrivateKey) parseMldsaEcdsaPrivateKey(data []byte, ecLen, dLen int) (err error) { - if pk.Version != 6 { - return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + ECDSA key") - } - pub := pk.PublicKey.PublicKey.(*mldsa_ecdsa.PublicKey) - priv := new(mldsa_ecdsa.PrivateKey) - priv.PublicKey = *pub - - buf := bytes.NewBuffer(data) - ec := encoding.NewEmptyOctetArray(ecLen) - if _, err := ec.ReadFrom(buf); err != nil { - return err - } - - d := encoding.NewEmptyOctetArray(dLen) - if _, err := d.ReadFrom(buf); err != nil { - return err - } - - err = priv.UnmarshalIntegerSecret(ec.Bytes()) - if err != nil { - return err - } - - priv.SecretMldsa = priv.Mldsa.PrivateKeyFromBytes(d.Bytes()) - if err := mldsa_ecdsa.Validate(priv); err != nil { - return err - } - pk.PrivateKey = priv - - return nil -} - // parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in // https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, dLen int) (err error) { diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 85ac9e49..0b680336 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -7,7 +7,6 @@ package packet import ( "bytes" "crypto/dsa" - "crypto/elliptic" "crypto/rsa" "crypto/sha1" "crypto/sha256" @@ -21,8 +20,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/brainpool" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" @@ -303,20 +300,6 @@ func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *P return pk } -func NewMldsaEcdsaPublicKey(creationTime time.Time, pub *mldsa_ecdsa.PublicKey) *PublicKey { - pk := &PublicKey{ - Version: 6, - CreationTime: creationTime, - PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), - PublicKey: pub, - p: encoding.NewOctetArray(pub.MarshalPoint()), - q: encoding.NewOctetArray(pub.PublicMldsa.Bytes()), - } - - pk.setFingerprintAndKeyId() - return pk -} - func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) *PublicKey { pk := &PublicKey{ Version: 6, @@ -389,18 +372,10 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { err = pk.parseMlkemEcdh(r, 32, mlkem768.PublicKeySize) case PubKeyAlgoMlkem1024X448: err = pk.parseMlkemEcdh(r, 56, mlkem1024.PublicKeySize) - case PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: - err = pk.parseMlkemEcdh(r, 65, mlkem768.PublicKeySize) - case PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: - err = pk.parseMlkemEcdh(r, 97, mlkem1024.PublicKeySize) case PubKeyAlgoMldsa65Ed25519: err = pk.parseMldsaEddsa(r, 32, dilithium.MLDSA65.PublicKeySize()) case PubKeyAlgoMldsa87Ed448: err = pk.parseMldsaEddsa(r, 57, dilithium.MLDSA87.PublicKeySize()) - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: - err = pk.parseMldsaEcdsa(r, 65, dilithium.MLDSA65.PublicKeySize()) - case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: - err = pk.parseMldsaEcdsa(r, 97, dilithium.MLDSA87.PublicKeySize()) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -790,42 +765,6 @@ func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { return bindingHash, err } -// parseMldsaEcdsa parses a ML-DSA + ECDSA public key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 -func (pk *PublicKey) parseMldsaEcdsa(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_ecdsa.PublicKey{ - AlgId: uint8(pk.PubKeyAlgo), - } - - if pub.Curve, err = GetEcdsaCurveFromAlgID(pk.PubKeyAlgo); err != nil { - return err - } - - if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { - return err - } - - if err := pub.UnmarshalPoint(pk.p.Bytes()); err != nil { - return err - } - - pub.PublicMldsa = pub.Mldsa.PublicKeyFromBytes(pk.q.Bytes()) - - pk.PublicKey = pub - - return -} - // parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in // https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { @@ -948,10 +887,8 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: length += 1 // Hash octet length += 32 // Binding hash - case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, - PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, - PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: length += uint32(pk.p.EncodedLength()) length += uint32(pk.q.EncodedLength()) default: @@ -1062,10 +999,8 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { } _, err = w.Write(symmKey.BindingHash[:]) return - case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, - PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, - PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: if _, err = w.Write(pk.p.EncodedBytes()); err != nil { return } @@ -1174,13 +1109,6 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("mldsa_eddsa verification failure") } return nil - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, - PubKeyAlgoMldsa87Brainpool384: - mldsaEcdsaPublicKey := pk.PublicKey.(*mldsa_ecdsa.PublicKey) - if !mldsa_ecdsa.Verify(mldsaEcdsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.ECDSASigR.Bytes(), sig.ECDSASigS.Bytes()) { - return errors.SignatureError("mldsa_ecdsa verification failure") - } - return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -1411,10 +1339,8 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed448.PublicKeySize * 8 case ExperimentalPubKeyAlgoAEAD: bitLength = 32 - case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem1024P384, - PubKeyAlgoMlkem768Brainpool256, PubKeyAlgoMlkem1024Brainpool384, PubKeyAlgoMldsa65Ed25519, - PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, - PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: bitLength = pk.q.BitLength() // Very questionable default: err = errors.InvalidArgumentError("bad public-key algorithm") @@ -1461,14 +1387,6 @@ func GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { return PubKeyAlgoMlkem768X25519, nil case PubKeyAlgoMldsa87Ed448: return PubKeyAlgoMlkem1024X448, nil - case PubKeyAlgoMldsa65p256: - return PubKeyAlgoMlkem768P256, nil - case PubKeyAlgoMldsa87p384: - return PubKeyAlgoMlkem1024P384, nil - case PubKeyAlgoMldsa65Brainpool256: - return PubKeyAlgoMlkem768Brainpool256, nil - case PubKeyAlgoMldsa87Brainpool384: - return PubKeyAlgoMlkem1024Brainpool384, nil default: return 0, goerrors.New("packet: unsupported pq public key algorithm") } @@ -1477,9 +1395,9 @@ func GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { // GetMlkemFromAlgID returns the ML-KEM instance from the matching KEM func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) { switch algId { - case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem768P256, PubKeyAlgoMlkem768Brainpool256: + case PubKeyAlgoMlkem768X25519: return mlkem768.Scheme(), nil - case PubKeyAlgoMlkem1024X448, PubKeyAlgoMlkem1024P384, PubKeyAlgoMlkem1024Brainpool384: + case PubKeyAlgoMlkem1024X448: return mlkem1024.Scheme(), nil default: return nil, goerrors.New("packet: unsupported ML-KEM public key algorithm") @@ -1493,34 +1411,11 @@ func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { return ecc.NewCurve25519(), nil case PubKeyAlgoMlkem1024X448: return ecc.NewX448(), nil - case PubKeyAlgoMlkem768P256: - return ecc.NewGenericCurve(elliptic.P256()), nil - case PubKeyAlgoMlkem1024P384: - return ecc.NewGenericCurve(elliptic.P384()), nil - case PubKeyAlgoMlkem768Brainpool256: - return ecc.NewGenericCurve(brainpool.P256r1()), nil - case PubKeyAlgoMlkem1024Brainpool384: - return ecc.NewGenericCurve(brainpool.P384r1()), nil default: return nil, goerrors.New("packet: unsupported ECDH public key algorithm") } } -func GetEcdsaCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDSACurve, error) { - switch algId { - case PubKeyAlgoMldsa65p256: - return ecc.NewGenericCurve(elliptic.P256()), nil - case PubKeyAlgoMldsa87p384: - return ecc.NewGenericCurve(elliptic.P384()), nil - case PubKeyAlgoMldsa65Brainpool256: - return ecc.NewGenericCurve(brainpool.P256r1()), nil - case PubKeyAlgoMldsa87Brainpool384: - return ecc.NewGenericCurve(brainpool.P384r1()), nil - default: - return nil, goerrors.New("packet: unsupported ECDSA public key algorithm") - } -} - func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { switch algId { case PubKeyAlgoMldsa65Ed25519: @@ -1534,9 +1429,9 @@ func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (dilithium.Mode, error) { switch algId { - case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: + case PubKeyAlgoMldsa65Ed25519: return dilithium.MLDSA65, nil - case PubKeyAlgoMldsa87Ed448, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: + case PubKeyAlgoMldsa87Ed448: return dilithium.MLDSA87, 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 47916e58..054b60c3 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -14,7 +14,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/cloudflare/circl/sign/dilithium" @@ -181,8 +180,7 @@ 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, PubKeyAlgoMldsa65p256, - PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, PubKeyAlgoMldsa87Brainpool384: + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -333,14 +331,6 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err = sig.parseMldsaEddsaSignature(r, 114, dilithium.MLDSA87.SignatureSize()); err != nil { return } - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa65Brainpool256: - if err = sig.parseMldsaEcdsaSignature(r, 32, dilithium.MLDSA65.SignatureSize()); err != nil { - return - } - case PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa87Brainpool384: - if err = sig.parseMldsaEcdsaSignature(r, 48, dilithium.MLDSA87.SignatureSize()); err != nil { - return - } default: panic("unreachable") } @@ -360,24 +350,6 @@ func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (er return } -// parseMldsaEcdsaSignature parses a ML-DSA + ECDSA signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-packet-tag-2 -func (sig *Signature) parseMldsaEcdsaSignature(r io.Reader, ecLen, dLen int) (err error) { - sig.ECDSASigR = encoding.NewEmptyOctetArray(ecLen) - if _, err = sig.ECDSASigR.ReadFrom(r); err != nil { - return - } - - sig.ECDSASigS = encoding.NewEmptyOctetArray(ecLen) - if _, err = sig.ECDSASigS.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 4880, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -1023,19 +995,6 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e if err == nil { sig.HMAC = encoding.NewShortByteString(sigdata) } - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, - PubKeyAlgoMldsa87Brainpool384: - if sig.Version != 6 { - return errors.UnsupportedError("cannot use mldsa_ecdsa on a non-v6 signature") - } - sk := priv.PrivateKey.(*mldsa_ecdsa.PrivateKey) - dSig, ecR, ecS, err := mldsa_ecdsa.Sign(config.Random(), sk, digest) - - if err == nil { - sig.MldsaSig = encoding.NewOctetArray(dSig) - sig.ECDSASigR = encoding.NewOctetArray(ecR) - sig.ECDSASigS = encoding.NewOctetArray(ecS) - } case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: if sig.Version != 6 { return errors.UnsupportedError("cannot use mldsa_eddsa on a non-v6 signature") @@ -1178,11 +1137,6 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: sigLength = int(sig.EdDSASigR.EncodedLength()) sigLength += int(sig.MldsaSig.EncodedLength()) - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, - PubKeyAlgoMldsa87Brainpool384: - sigLength = int(sig.ECDSASigR.EncodedLength()) - sigLength += int(sig.ECDSASigS.EncodedLength()) - sigLength += int(sig.MldsaSig.EncodedLength()) default: panic("impossible") } @@ -1296,15 +1250,6 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { return } _, err = w.Write(sig.MldsaSig.EncodedBytes()) - case PubKeyAlgoMldsa65p256, PubKeyAlgoMldsa87p384, PubKeyAlgoMldsa65Brainpool256, - PubKeyAlgoMldsa87Brainpool384: - if _, err = w.Write(sig.ECDSASigR.EncodedBytes()); err != nil { - return - } - if _, err = w.Write(sig.ECDSASigS.EncodedBytes()); err != nil { - return - } - _, err = w.Write(sig.MldsaSig.EncodedBytes()) default: panic("impossible") } diff --git a/openpgp/pqc_vectors_test.go b/openpgp/pqc_vectors_test.go index da6451eb..112a7705 100644 --- a/openpgp/pqc_vectors_test.go +++ b/openpgp/pqc_vectors_test.go @@ -1,17 +1,12 @@ -// 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. - -//go:build pqc_test_vectors - package openpgp import ( "bytes" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" "strings" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) func dumpTestVector(t *testing.T, filename, vector string) { @@ -86,40 +81,6 @@ func encryptPqcMessageVector(t *testing.T, filename string, entity *Entity, conf } func TestV4EddsaPqKey(t *testing.T) { - //eddsaConfig := &packet.Config{ - // DefaultHash: crypto.SHA512, - // Algorithm: packet.PubKeyAlgoEdDSA, - // V6Keys: false, - // 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: false, - // 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) - //} - entities, err := ReadArmoredKeyRing(strings.NewReader(v4Ed25519Mlkem768X25519PrivateTestVector)) if err != nil { t.Error(err) @@ -154,41 +115,6 @@ func TestV4EddsaPqKey(t *testing.T) { } 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 - // }, - //} - // - //entity.Subkeys = []Subkey{} - //err = entity.AddEncryptionSubkey(kyberConfig) - //if err != nil { - // t.Fatal(err) - //} - entities, err := ReadArmoredKeyRing(strings.NewReader(v6Ed25519Mlkem768X25519PrivateTestVector)) if err != nil { t.Error(err) diff --git a/openpgp/read.go b/openpgp/read.go index 0a43c589..1a5f18d0 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -123,8 +123,7 @@ ParsePackets: switch p.Algo { case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, - packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, packet.PubKeyAlgoMlkem1024P384, - packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index ed647656..fe1a2125 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,7 +21,6 @@ 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_ecdsa" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" @@ -394,22 +393,6 @@ 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.PubKeyAlgoMldsa65p256, packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384: - if !config.V6() { - return nil, goerrors.New("openpgp: cannot create a non-v6 ML-DSA + ECDSA key") - } - - c, err := packet.GetEcdsaCurveFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) - if err != nil { - return nil, err - } - - return mldsa_ecdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if !config.V6() { return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") @@ -471,15 +454,12 @@ 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, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, - packet.PubKeyAlgoMldsa87Brainpool384: + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: if pubKeyAlgo, err = packet.GetMatchingMlkemKem(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, packet.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) if err != nil { diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index b71ad041..417bda09 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -141,8 +141,7 @@ ParsePackets: switch p.Algo { case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, - packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, packet.PubKeyAlgoMlkem1024P384, - packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384: + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go index 1d113255..db7e39cb 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -212,10 +212,8 @@ func (s *Subkey) LatestValidBindingSignature(date time.Time, config *packet.Conf // 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.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, packet.PubKeyAlgoMlkem1024Brainpool384, - packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoMldsa65p256, - packet.PubKeyAlgoMldsa87p384, packet.PubKeyAlgoMldsa65Brainpool256, packet.PubKeyAlgoMldsa87Brainpool384: + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: return true default: return false diff --git a/openpgp/write_test.go b/openpgp/write_test.go index 421d79e8..fa1d91ad 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -878,9 +878,7 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. switch p.Algo { case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, packet.PubKeyAlgoMlkem768P256, - packet.PubKeyAlgoMlkem1024P384, packet.PubKeyAlgoMlkem768Brainpool256, - packet.PubKeyAlgoMlkem1024Brainpool384: + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue From 972f2c6c44a48aeb5c796169cf2fcec1cabf5c90 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 14:19:02 +0200 Subject: [PATCH 26/36] Update links to PQC draft-rfc --- openpgp/mldsa_eddsa/mldsa_eddsa.go | 17 ++++++++++------- openpgp/mlkem_ecdh/mlkem_ecdh.go | 21 ++++++++++++--------- openpgp/packet/private_key.go | 8 ++++---- openpgp/packet/public_key.go | 4 ++-- openpgp/packet/signature.go | 2 +- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go index d64f2718..87f811d5 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -1,5 +1,5 @@ // Package mldsa_eddsa implements hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. -// It follows the specs https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-signature-schemes +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-composite-signature-schemes package mldsa_eddsa import ( @@ -27,7 +27,7 @@ type PrivateKey struct { } // GenerateKey generates a ML-DSA + EdDSA composite key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation-procedure-2 +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-generation-procedure-2 func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d dilithium.Mode) (priv *PrivateKey, err error) { priv = new(PrivateKey) @@ -41,11 +41,14 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d dilithium.Mode } priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey(rand) - return + if err != nil { + return nil, err + } + return priv, nil } // Sign generates a ML-DSA + EdDSA composite signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-generation +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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 { @@ -57,11 +60,11 @@ func Sign(priv *PrivateKey, message []byte) (dSig, ecSig []byte, err error) { return nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") } - return + return dSig, ecSig, nil } // Verify verifies a ML-DSA + EdDSA composite signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-verification +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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) } @@ -81,5 +84,5 @@ func Validate(priv *PrivateKey) (err error) { if subtle.ConstantTimeCompare(priv.PublicMldsa.Bytes(), casted.Bytes()) == 0 { return errors.KeyInvalidError("mldsa_eddsa: invalid public key") } - return + return nil } diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index dc61a4dd..8e6f1015 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -1,5 +1,5 @@ // Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. -// It follows the spec https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-composite-kem-schemes +// It follows the spec https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-composite-kem-schemes package mlkem_ecdh import ( @@ -30,7 +30,7 @@ type PrivateKey struct { } // GenerateKey implements ML-KEM + ECC key generation as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-generation-procedure +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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) @@ -44,8 +44,8 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (pr } kyberSeed := make([]byte, k.SeedSize()) - _, err = rand.Read(kyberSeed) - if err != nil { + + if _, err = rand.Read(kyberSeed); err != nil { return nil, err } @@ -54,7 +54,7 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (pr } // Encrypt implements ML-KEM + ECC encryption as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-encryption-procedure +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-encryption-procedure func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { if len(msg) > 64 { return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") @@ -95,7 +95,7 @@ func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemera } // Decrypt implements ML-KEM + ECC decryption as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-decryption-procedure +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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) @@ -118,7 +118,7 @@ func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg } // buildKey implements the composite KDF as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-combiner +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-combiner func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { h := sha3.New256() @@ -192,8 +192,11 @@ func EncodeFields(w io.Writer, ec, ml, encryptedSessionKey []byte, cipherFunctio } } - _, err = w.Write(encryptedSessionKey) - return err + if _, err = w.Write(encryptedSessionKey); err != nil { + return err + } + + return nil } // DecodeFields decodes an ML-KEM + ECDH session key encryption fields as diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index e0f1a2ba..9ea8d29f 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -565,7 +565,7 @@ func serializeHMACPrivateKey(w io.Writer, priv *symmetric.HMACPrivateKey) (err e } // serializeMlkemPrivateKey serializes a ML-KEM + ECC private key according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-material-packets func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err error) { var kyberBin []byte if kyberBin, err = priv.SecretMlkem.MarshalBinary(); err != nil { @@ -579,7 +579,7 @@ func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err err } // serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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 @@ -1260,7 +1260,7 @@ func validateCommonSymmetric(seed [32]byte, bindingHash [32]byte) error { } // parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-material-packets-2 func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, dLen int) (err error) { if pk.Version != 6 { return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + EdDSA key") @@ -1291,7 +1291,7 @@ func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, dLen int) (e } // parseMlkemEcdhPrivateKey parses a ML-KEM + ECC private key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-material-packets func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, kLen int) (err error) { pub := pk.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey) priv := new(mlkem_ecdh.PrivateKey) diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 0b680336..f25c1199 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -586,7 +586,7 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { } // parseMlkemEcdh parses a ML-KEM + ECC public key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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 { @@ -766,7 +766,7 @@ func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { } // parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-key-material-packets-2 +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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 { diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 054b60c3..9f836cad 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -338,7 +338,7 @@ func (sig *Signature) parse(r io.Reader) (err error) { } // parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in -// https://www.ietf.org/archive/id/draft-wussler-openpgp-pqc-03.html#name-signature-packet-tag-2 +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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 { From 28c613e7c7199bdde3391e872871402b35ee0d38 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 15:58:02 +0200 Subject: [PATCH 27/36] feat: Update to latest circle version - Update to Fips compliant algorithms --- go.mod | 2 +- go.sum | 55 +--------------- openpgp/integration_tests/v2/utils_test.go | 14 ++-- openpgp/keys_v6_test.go | 9 ++- openpgp/mldsa_eddsa/mldsa_eddsa.go | 24 +++---- openpgp/mldsa_eddsa/mldsa_eddsa_test.go | 10 ++- openpgp/packet/private_key.go | 22 +++++-- openpgp/packet/public_key.go | 24 ++++--- openpgp/packet/signature.go | 7 +- openpgp/read_write_test_data.go | 2 +- openpgp/v2/read_test.go | 7 ++ openpgp/v2/read_write_test_data.go | 2 +- openpgp/v2/write_test.go | 74 +--------------------- openpgp/write_test.go | 74 +--------------------- 14 files changed, 85 insertions(+), 241 deletions(-) diff --git a/go.mod b/go.mod index 43bad186..9c55b151 100644 --- a/go.mod +++ b/go.mod @@ -9,4 +9,4 @@ require ( require golang.org/x/sys v0.22.0 // indirect -replace github.com/cloudflare/circl v1.3.7 => github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2 +replace github.com/cloudflare/circl v1.3.7 => github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630 diff --git a/go.sum b/go.sum index e5e1461d..a0044369 100644 --- a/go.sum +++ b/go.sum @@ -1,57 +1,6 @@ -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v0.0.0-20240227155846-ede8f45b4d37/go.mod h1:Rtp2DgaIOIqDrWkeSBF4qtj92/5YQzSwE4QRH+px1bs= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c h1:JCxCKz59IXghzSSUstoaWa7h7lZdmd0LFMiMfF56ECk= -github.com/kasperdi/SPHINCSPLUS-golang v0.0.0-20221227220735-de985e5a663c/go.mod h1:+SeUKO8dPlXRdYr4SK+UIs8SLz0Dl3ZceKdXGaSFsFY= -github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2 h1:b1oBEyYCXXr5y+OxdGmXbKuKlm526nhJnPFmJ0pGFGs= -github.com/wussler/circl v0.0.0-20240227155518-22e2dd8861f2/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= -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.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630 h1:XWvuFImxUQ/UUTVC6Po3jtEM/c5V6Cc+8KmCe5DEfko= +github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 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/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -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-20210423082822-04245dca01da/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/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.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.6/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= diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index 0c3c49c3..ef9c18bf 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/keys_v6_test.go b/openpgp/keys_v6_test.go index 0e74d44a..7914d3eb 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -264,9 +264,14 @@ func TestGeneratePqKey(t *testing.T) { } if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { - bin := pk.PublicMldsa.Bytes() + bin, err := pk.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } bin[5] ^= 1 - pk.PublicMldsa = pk.Mldsa.PublicKeyFromBytes(bin) + if pk.PublicMldsa, err = pk.Mldsa.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal(err) + } } err = read.PrivateKey.Decrypt(randomPassword) diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go index 87f811d5..5ae876c8 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -3,32 +3,31 @@ package mldsa_eddsa import ( - "crypto/subtle" goerrors "errors" "io" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" - "github.com/cloudflare/circl/sign/dilithium" + "github.com/cloudflare/circl/sign" ) type PublicKey struct { AlgId uint8 Curve ecc.EdDSACurve - Mldsa dilithium.Mode + Mldsa sign.Scheme PublicPoint []byte - PublicMldsa dilithium.PublicKey + PublicMldsa sign.PublicKey } type PrivateKey struct { PublicKey SecretEc []byte - SecretMldsa dilithium.PrivateKey + SecretMldsa sign.PrivateKey } // GenerateKey generates a ML-DSA + EdDSA composite key as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-generation-procedure-2 -func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d dilithium.Mode) (priv *PrivateKey, err error) { +func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d sign.Scheme) (priv *PrivateKey, err error) { priv = new(PrivateKey) priv.PublicKey.AlgId = algId @@ -40,7 +39,7 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d dilithium.Mode return nil, err } - priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey(rand) + priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey() if err != nil { return nil, err } @@ -55,7 +54,7 @@ func Sign(priv *PrivateKey, message []byte) (dSig, ecSig []byte, err error) { return nil, nil, err } - dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message) + 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") } @@ -66,7 +65,7 @@ func Sign(priv *PrivateKey, message []byte) (dSig, ecSig []byte, err error) { // Verify verifies a ML-DSA + EdDSA composite signature as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.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) + 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 @@ -75,14 +74,9 @@ func Validate(priv *PrivateKey) (err error) { return err } - pub := priv.SecretMldsa.Public() - casted, ok := pub.(dilithium.PublicKey) - if !ok { + if !priv.PublicMldsa.Equal(priv.SecretMldsa.Public()) { return errors.KeyInvalidError("mldsa_eddsa: invalid public key") } - if subtle.ConstantTimeCompare(priv.PublicMldsa.Bytes(), casted.Bytes()) == 0 { - 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 index b848c330..20b9e160 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -31,9 +31,15 @@ func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { t.Fatalf("valid key marked as invalid: %s", err) } - bin := key.PublicMldsa.Bytes() + bin, err := key.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } bin[5] ^= 1 - key.PublicMldsa = key.Mldsa.PublicKeyFromBytes(bin) + 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") diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 9ea8d29f..7be561da 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -23,7 +23,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" "github.com/cloudflare/circl/kem/mlkem/mlkem768" - "github.com/cloudflare/circl/sign/dilithium" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" @@ -584,8 +585,14 @@ func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) er if _, err := w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { return err } - _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsa.Bytes()).EncodedBytes()) - return err + bin, err := priv.SecretMldsa.MarshalBinary() + if err != nil { + return err + } + if _, err = w.Write(encoding.NewOctetArray(bin).EncodedBytes()); err != nil { + return err + } + return nil } // decrypt decrypts an encrypted private key using a decryption key. @@ -933,9 +940,9 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { case PubKeyAlgoMlkem1024X448: return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem1024.PrivateKeySize) case PubKeyAlgoMldsa65Ed25519: - return pk.parseMldsaEddsaPrivateKey(data, 32, dilithium.MLDSA65.PrivateKeySize()) + return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa65.PrivateKeySize) case PubKeyAlgoMldsa87Ed448: - return pk.parseMldsaEddsaPrivateKey(data, 57, dilithium.MLDSA87.PrivateKeySize()) + return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa87.PrivateKeySize) default: err = errors.StructuralError("unknown private key type") return @@ -1281,7 +1288,10 @@ func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, dLen int) (e } priv.SecretEc = ec.Bytes() - priv.SecretMldsa = priv.Mldsa.PrivateKeyFromBytes(d.Bytes()) + priv.SecretMldsa, err = priv.Mldsa.UnmarshalBinaryPrivateKey(d.Bytes()) + if err != nil { + return err + } if err := mldsa_eddsa.Validate(priv); err != nil { return err } diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f25c1199..6eea9d3b 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -24,7 +24,9 @@ import ( "github.com/cloudflare/circl/kem" "github.com/cloudflare/circl/kem/mlkem/mlkem1024" "github.com/cloudflare/circl/kem/mlkem/mlkem768" - "github.com/cloudflare/circl/sign/dilithium" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" @@ -301,13 +303,17 @@ func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *P } 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(pub.PublicMldsa.Bytes()), + q: encoding.NewOctetArray(publicKeyBytes), } pk.setFingerprintAndKeyId() @@ -373,9 +379,9 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { case PubKeyAlgoMlkem1024X448: err = pk.parseMlkemEcdh(r, 56, mlkem1024.PublicKeySize) case PubKeyAlgoMldsa65Ed25519: - err = pk.parseMldsaEddsa(r, 32, dilithium.MLDSA65.PublicKeySize()) + err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize) case PubKeyAlgoMldsa87Ed448: - err = pk.parseMldsaEddsa(r, 57, dilithium.MLDSA87.PublicKeySize()) + err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -791,7 +797,9 @@ func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { return err } - pub.PublicMldsa = pub.Mldsa.PublicKeyFromBytes(pk.q.Bytes()) + if pub.PublicMldsa, err = pub.Mldsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } pk.PublicKey = pub return @@ -1427,12 +1435,12 @@ func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { } } -func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (dilithium.Mode, error) { +func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { switch algId { case PubKeyAlgoMldsa65Ed25519: - return dilithium.MLDSA65, nil + return mldsa65.Scheme(), nil case PubKeyAlgoMldsa87Ed448: - return dilithium.MLDSA87, nil + 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 9f836cad..79c7bad2 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -15,7 +15,8 @@ import ( "time" "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/cloudflare/circl/sign/dilithium" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -324,11 +325,11 @@ func (sig *Signature) parse(r io.Reader) (err error) { return } case PubKeyAlgoMldsa65Ed25519: - if err = sig.parseMldsaEddsaSignature(r, 64, dilithium.MLDSA65.SignatureSize()); err != nil { + if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { return } case PubKeyAlgoMldsa87Ed448: - if err = sig.parseMldsaEddsaSignature(r, 114, dilithium.MLDSA87.SignatureSize()); err != nil { + if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { return } default: diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index f2a298c8..6b38f542 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -479,7 +479,7 @@ const v4Ed25519Mlkem768X25519PrivateHex = "c5580451d0c68016092b06010401da470f010 const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" -const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" +//const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" // PQC draft test vectors const v4Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 87988f5f..ac140049 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/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 { diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 667b78f5..51d2027c 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -764,7 +764,7 @@ const v4Ed25519Mlkem768X25519PrivateHex = "c5580451d0c68016092b06010401da470f010 const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" -const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" +// const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" // PQC draft test vectors const v4Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index 3d834263..ef136665 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -693,7 +693,7 @@ var testEncryptionTests = map[string]struct { true, true, }, - "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + /*"v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { mldsa65Ed25519Mlkem768X25519PrivateHex, false, true, @@ -702,77 +702,7 @@ var testEncryptionTests = map[string]struct { mldsa65Ed25519Mlkem768X25519PrivateHex, true, true, - }, - //{ - // mldsa87Ed448Mlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87Ed448Mlkem1024X448PrivateHex, - // true, - // true, - //}, - //{ - // mldsa65P256Mlkem768P245PrivateHex, - // false, - // true, - //}, - //{ - // mldsa65P256Mlkem768P245PrivateHex, - // true, - // true, - //}, - //{ - // mldsa87P384_Mlkem1024P384PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87P384_Mlkem1024P384PrivateHex, - // true, - // true, - //}, - //{ - // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, - // false, - // true, - //}, - //{ - // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, - // true, - // true, - //}, - //{ - // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, - // true, - // true, - //}, - //{ - // slhDsaSha2Mlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // slhDsaSha2Mlkem1024X448PrivateHex, - // true, - // true, - //}, - //{ - // slhDsaShakeMlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // slhDsaShakeMlkem1024X448PrivateHex, - // true, - // true, - //}, + },*/ } func TestEncryption(t *testing.T) { diff --git a/openpgp/write_test.go b/openpgp/write_test.go index fa1d91ad..b6e3d7ed 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -567,7 +567,7 @@ var testEncryptionTests = map[string]struct { true, true, }, - "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + /*"v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { mldsa65Ed25519Mlkem768X25519PrivateHex, false, true, @@ -576,77 +576,7 @@ var testEncryptionTests = map[string]struct { mldsa65Ed25519Mlkem768X25519PrivateHex, true, true, - }, - //{ - // mldsa87Ed448Mlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87Ed448Mlkem1024X448PrivateHex, - // true, - // true, - //}, - //{ - // mldsa65P256Mlkem768P245PrivateHex, - // false, - // true, - //}, - //{ - // mldsa65P256Mlkem768P245PrivateHex, - // true, - // true, - //}, - //{ - // mldsa87P384_Mlkem1024P384PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87P384_Mlkem1024P384PrivateHex, - // true, - // true, - //}, - //{ - // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, - // false, - // true, - //}, - //{ - // mldsa65Brainpool256Mlkem768Brainpool256PrivateHex, - // true, - // true, - //}, - //{ - // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, - // false, - // true, - //}, - //{ - // mldsa87Brainpool384Mlkem1024Brainpool384PrivateHex, - // true, - // true, - //}, - //{ - // slhDsaSha2Mlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // slhDsaSha2Mlkem1024X448PrivateHex, - // true, - // true, - //}, - //{ - // slhDsaShakeMlkem1024X448PrivateHex, - // false, - // true, - //}, - //{ - // slhDsaShakeMlkem1024X448PrivateHex, - // true, - // true, - //}, + },*/ } func TestEncryption(t *testing.T) { From 820b6b50eacc395b8bc6e4e9c29c0b50785fd470 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Thu, 12 Sep 2024 16:26:16 +0200 Subject: [PATCH 28/36] feat: Derive ML-DSA keys from seed --- openpgp/mldsa_eddsa/mldsa_eddsa.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go index 5ae876c8..34c32d8b 100644 --- a/openpgp/mldsa_eddsa/mldsa_eddsa.go +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -39,10 +39,13 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d sign.Scheme) ( return nil, err } - priv.PublicKey.PublicMldsa, priv.SecretMldsa, err = priv.PublicKey.Mldsa.GenerateKey() - if err != nil { + keySeed := make([]byte, d.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { return nil, err } + + priv.PublicKey.PublicMldsa, priv.SecretMldsa = priv.PublicKey.Mldsa.DeriveKey(keySeed) + return priv, nil } From a993e707fbab5e3a4bf458233bea797c2e43fbe5 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Tue, 24 Sep 2024 15:27:02 +0200 Subject: [PATCH 29/36] 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 7ef309d6..c5bade07 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 From 2e3a702948cc43652316f6fb5b994525c4ab5965 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Tue, 24 Sep 2024 15:49:42 +0200 Subject: [PATCH 30/36] feat: Fallback to AES256 if all recipients are PQ --- openpgp/keys.go | 11 ++--------- openpgp/packet/public_key.go | 11 +++++++++++ openpgp/v2/write.go | 15 ++++++++++++--- openpgp/write.go | 13 +++++++++++-- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/openpgp/keys.go b/openpgp/keys.go index 62a60b77..34bffb97 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -310,16 +310,9 @@ func (s *Subkey) Revoked(now time.Time) bool { return revoked(s.Revocations, now) } -// IsPQ returns true if the algorithm is Post-Quantum safe +// 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 - } - + return s.PublicKey.IsPQ() } // Revoked returns whether the key or subkey has been revoked by a self-signature. diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index 6eea9d3b..b72d1044 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -1389,6 +1389,17 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { 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 GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { switch algId { case PubKeyAlgoMldsa65Ed25519: diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 09ddc253..2587c963 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,9 +649,12 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // Todo: check PQC and use AES-256 - // 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]) diff --git a/openpgp/write.go b/openpgp/write.go index b0f6ef7b..1e247f57 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -391,6 +391,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 +399,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 +429,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]) From 8be8c23a7579b196d4aaa4c2d8a41cf93611d4b9 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Tue, 24 Sep 2024 16:06:52 +0200 Subject: [PATCH 31/36] refactor: Improve mlkem readability --- openpgp/mlkem_ecdh/mlkem_ecdh.go | 52 ++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index 8e6f1015..cb007d20 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -4,6 +4,7 @@ package mlkem_ecdh import ( goerrors "errors" + "fmt" "io" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" @@ -15,6 +16,11 @@ import ( "github.com/cloudflare/circl/kem" ) +const ( + maxSessionKeyLength = 64 + kdfContext = "OpenPGPCompositeKDFv1" +) + type PublicKey struct { AlgId uint8 Curve ecc.ECDHCurve @@ -43,9 +49,8 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (pr return nil, err } - kyberSeed := make([]byte, k.SeedSize()) - - if _, err = rand.Read(kyberSeed); err != nil { + kyberSeed, err := generateRandomSeed(rand, k.SeedSize()) + if err != nil { return nil, err } @@ -56,7 +61,7 @@ func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (pr // Encrypt implements ML-KEM + ECC encryption as specified in // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-encryption-procedure func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { - if len(msg) > 64 { + if len(msg) > maxSessionKeyLength { return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") } @@ -71,8 +76,7 @@ func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemera } // ML-KEM shared secret derivation - kyberSeed := make([]byte, pub.Mlkem.EncapsulationSeedSize()) - _, err = rand.Read(kyberSeed) + kyberSeed, err := generateRandomSeed(rand, pub.Mlkem.EncapsulationSeedSize()) if err != nil { return nil, nil, nil, err } @@ -82,12 +86,12 @@ func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemera return nil, nil, nil, err } - kek, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS, kEphemeral, pub.PublicMlkem) + 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(kek, msg); err != nil { + if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { return nil, nil, nil, err } @@ -136,20 +140,20 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK // eccData = eccKeyShare || eccCipherText // mlkemData = mlkemKeyShare || mlkemCipherText // encData = counter || eccData || mlkemData || fixedInfo - k := sha3.New256() + h.Reset() // SHA3 never returns error - _, _ = k.Write([]byte{0x00, 0x00, 0x00, 0x01}) - _, _ = k.Write(eccKeyShare) - _, _ = k.Write(eccEphemeral) - _, _ = k.Write(eccPublicKey) - _, _ = k.Write(mlkemKeyShare) - _, _ = k.Write(mlkemEphemeral) - _, _ = k.Write(serializedMlkemKey) - _, _ = k.Write([]byte{pub.AlgId}) - _, _ = k.Write([]byte("OpenPGPCompositeKDFv1")) - - return k.Sum(nil), nil + _, _ = h.Write([]byte{0x00, 0x00, 0x00, 0x01}) + _, _ = h.Write(eccKeyShare) + _, _ = h.Write(eccEphemeral) + _, _ = h.Write(eccPublicKey) + _, _ = h.Write(mlkemKeyShare) + _, _ = h.Write(mlkemEphemeral) + _, _ = h.Write(serializedMlkemKey) + _, _ = h.Write([]byte{pub.AlgId}) + _, _ = h.Write([]byte(kdfContext)) + + return h.Sum(nil), nil } // Validate checks that the public key corresponds to the private key @@ -237,3 +241,11 @@ func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, en 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 +} From e9782f882daa85cf9faeeb1386f5514040e3282f Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 27 Sep 2024 09:16:17 +0200 Subject: [PATCH 32/36] feat: Integrate review feedback --- openpgp/key_generation.go | 8 +- openpgp/keys_test.go | 19 --- openpgp/packet/encrypted_key.go | 3 +- openpgp/packet/private_key.go | 11 +- openpgp/packet/public_key.go | 21 ++-- openpgp/packet/signature.go | 7 +- openpgp/pqc_vectors_test.go | 142 --------------------- openpgp/v2/key_generation.go | 5 +- openpgp/v2/pqc_vectors_test.go | 217 -------------------------------- 9 files changed, 28 insertions(+), 405 deletions(-) delete mode 100644 openpgp/pqc_vectors_test.go delete mode 100644 openpgp/v2/pqc_vectors_test.go diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index a60b8b68..73fb56e0 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -13,8 +13,6 @@ import ( "math/big" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -23,6 +21,7 @@ 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" @@ -377,11 +376,14 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: - if pubKeyAlgo, err = packet.GetMatchingMlkemKem(config.PublicKeyAlgorithm()); err != nil { + 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 { diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 09f50dc5..8bddbeb7 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -2085,25 +2085,6 @@ TxGVotQ4A/0u0VbOMEUfnrI8Fms= t.Errorf("Expected AEAD subkey") } } -func TestAddV4MlkemSubkey(t *testing.T) { - eddsaConfig := &packet.Config{ - DefaultHash: crypto.SHA512, - Algorithm: packet.PubKeyAlgoEdDSA, - V6Keys: false, - DefaultCipher: packet.CipherAES256, - 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, false) -} func testAddMlkemSubkey(t *testing.T, entity *Entity, v6Keys bool) { var err error diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 0c4bd3a7..2da33cb1 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -14,13 +14,12 @@ import ( "math/big" "strconv" - "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" - "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 7be561da..b0cc8b8f 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -20,12 +20,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/cloudflare/circl/kem/mlkem/mlkem1024" - "github.com/cloudflare/circl/kem/mlkem/mlkem768" - "github.com/cloudflare/circl/sign/mldsa/mldsa65" - "github.com/cloudflare/circl/sign/mldsa/mldsa87" - "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -34,11 +28,16 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/s2k" "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" "golang.org/x/crypto/hkdf" ) diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index b72d1044..f93723cd 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -20,14 +20,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "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" - "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" @@ -38,10 +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. @@ -290,7 +289,7 @@ func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *P } pk := &PublicKey{ - Version: 4, + Version: 6, CreationTime: creationTime, PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), PublicKey: pub, @@ -1349,7 +1348,7 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = 32 case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: - bitLength = pk.q.BitLength() // Very questionable + bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1400,7 +1399,7 @@ func (pg *PublicKey) IsPQ() bool { } } -func GetMatchingMlkemKem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { +func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { switch algId { case PubKeyAlgoMldsa65Ed25519: return PubKeyAlgoMlkem768X25519, nil diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 79c7bad2..cacbb80c 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -14,10 +14,6 @@ import ( "strconv" "time" - "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" - "github.com/cloudflare/circl/sign/mldsa/mldsa65" - "github.com/cloudflare/circl/sign/mldsa/mldsa87" - "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/ed25519" "github.com/ProtonMail/go-crypto/openpgp/ed448" @@ -25,6 +21,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 ( diff --git a/openpgp/pqc_vectors_test.go b/openpgp/pqc_vectors_test.go deleted file mode 100644 index 112a7705..00000000 --- a/openpgp/pqc_vectors_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package openpgp - -import ( - "bytes" - "strings" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func dumpTestVector(t *testing.T, filename, vector string) { - t.Logf("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, 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 TestV4EddsaPqKey(t *testing.T) { - entities, err := ReadArmoredKeyRing(strings.NewReader(v4Ed25519Mlkem768X25519PrivateTestVector)) - if err != nil { - t.Error(err) - return - } - - entity := entities[0] - - serializePqSkVector(t, "v4-eddsa-sample-pk.asc", entity, true) - serializePqPkVector(t, "v4-eddsa-sample-pk.asc", entity, true) - - t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) - for i, subkey := range entity.Subkeys { - t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) - } - - var configV1 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: nil, - } - - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1, true) - - var configV2 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: &packet.AEADConfig{ - DefaultMode: packet.AEADModeOCB, - }, - } - - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2, false) -} - -func TestV6EddsaPqKey(t *testing.T) { - entities, err := ReadArmoredKeyRing(strings.NewReader(v6Ed25519Mlkem768X25519PrivateTestVector)) - if err != nil { - t.Error(err) - return - } - - entity := entities[0] - - serializePqSkVector(t, "v6-eddsa-sample-pk.asc", entity, false) - serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) - - t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) - for i, subkey := range entity.Subkeys { - t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) - } - - var configV2 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: &packet.AEADConfig{ - DefaultMode: packet.AEADModeOCB, - }, - } - - encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) -} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index fe1a2125..167a4068 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -455,11 +455,14 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { cipher := algorithm.CipherFunction(config.Cipher()) return symmetric.AEADGenerateKey(config.Random(), cipher) case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: - if pubKeyAlgo, err = packet.GetMatchingMlkemKem(config.PublicKeyAlgorithm()); err != nil { + 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 { diff --git a/openpgp/v2/pqc_vectors_test.go b/openpgp/v2/pqc_vectors_test.go deleted file mode 100644 index 65d1265f..00000000 --- a/openpgp/v2/pqc_vectors_test.go +++ /dev/null @@ -1,217 +0,0 @@ -// 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. - -//go:build pqc_test_vectors - -package v2 - -import ( - "bytes" - "strings" - "testing" - - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -func dumpTestVector(t *testing.T, filename, vector string) { - t.Logf("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, 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 TestV4EddsaPqKey(t *testing.T) { - //eddsaConfig := &packet.Config{ - // DefaultHash: crypto.SHA512, - // Algorithm: packet.PubKeyAlgoEdDSA, - // V6Keys: false, - // 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: false, - // 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) - //} - - entities, err := ReadArmoredKeyRing(strings.NewReader(v4Ed25519Mlkem768X25519PrivateTestVector)) - if err != nil { - t.Error(err) - return - } - - entity := entities[0] - - serializePqSkVector(t, "v4-eddsa-sample-pk.asc", entity, true) - serializePqPkVector(t, "v4-eddsa-sample-pk.asc", entity, true) - - t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) - for i, subkey := range entity.Subkeys { - t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) - } - - var configV1 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: nil, - } - - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v1.asc", entity, configV1, true) - - var configV2 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: &packet.AEADConfig{ - DefaultMode: packet.AEADModeOCB, - }, - } - - encryptPqcMessageVector(t, "v4-eddsa-sample-message-v2.asc", entity, configV2, false) -} - -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 - // }, - //} - // - //entity.Subkeys = []Subkey{} - //err = entity.AddEncryptionSubkey(kyberConfig) - //if err != nil { - // t.Fatal(err) - //} - - entities, err := ReadArmoredKeyRing(strings.NewReader(v6Ed25519Mlkem768X25519PrivateTestVector)) - if err != nil { - t.Error(err) - return - } - - entity := entities[0] - - serializePqSkVector(t, "v6-eddsa-sample-pk.asc", entity, false) - serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) - - t.Logf("Primary fingerprint: %x", entity.PrimaryKey.Fingerprint) - for i, subkey := range entity.Subkeys { - t.Logf("Sub-key %d fingerprint: %x", i, subkey.PublicKey.Fingerprint) - } - - var configV2 = &packet.Config{ - DefaultCipher: packet.CipherAES256, - AEADConfig: &packet.AEADConfig{ - DefaultMode: packet.AEADModeOCB, - }, - } - - encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) -} From bcbd610904fa54ed7142c078bca7d7087cb18edf Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 11 Oct 2024 10:07:37 +0200 Subject: [PATCH 33/36] feat: Update circl to v1.5.0 --- go.mod | 6 ++---- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 9c55b151..25880a80 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module github.com/ProtonMail/go-crypto -go 1.21 +go 1.22.0 require ( - github.com/cloudflare/circl v1.3.7 + github.com/cloudflare/circl v1.5.0 golang.org/x/crypto v0.25.0 ) require golang.org/x/sys v0.22.0 // indirect - -replace github.com/cloudflare/circl v1.3.7 => github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630 diff --git a/go.sum b/go.sum index a0044369..1a97c0f3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630 h1:XWvuFImxUQ/UUTVC6Po3jtEM/c5V6Cc+8KmCe5DEfko= -github.com/lubux/circl v0.0.0-20240912122524-f16d68fe1630/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +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= From d8b79f7ec5974836d8033d43fd4a15154fc6dd10 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 11 Oct 2024 11:40:25 +0200 Subject: [PATCH 34/36] fix: Update curve448 integration test - the key is not expired anymore --- openpgp/integration_tests/testdata/test_vectors.json | 10 +++++----- .../integration_tests/v2/testdata/test_vectors.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpgp/integration_tests/testdata/test_vectors.json b/openpgp/integration_tests/testdata/test_vectors.json index 1943e624..d5fc0d25 100644 --- a/openpgp/integration_tests/testdata/test_vectors.json +++ b/openpgp/integration_tests/testdata/test_vectors.json @@ -16,12 +16,12 @@ "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEYvTtQBYJKwYBBAHaRw8BAQdAxsNXLbrk5xOjpO24VhOMvQ0/F+JcyIkckMDH\nX3FIGxfNIkdvbGFuZyBHb3BoZXIgPGdvbGFuZ0BleGFtcGxlLm9yZz7CkAQTFggA\nOBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsDBQsJCAcCBhUKCQgLAgQW\nAgMBAh4BAheAAAoJEAy9+ctDgkE3nJ4BAIp2vLwHlJwI+t6/b5QHIwKyztJ27lGG\niYIWwd008twoAQDy6B6L7WjAexX7dwbBZbMqTqwtrxk3zuLdIq44an0NDM44BGL0\n7UASCisGAQQBl1UBBQEBB0DkK5s+nyXuWtVDbmHsfHlc3YLzFqdGeelyPMSigJoQ\nDQMBCAfCeAQYFggAIBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsMAAoJ\nEAy9+ctDgkE3L3cBAO5W+YP2IrKxH4quutLsDBqHqS1H77ais7kOoXmHvN8tAQDe\nuRX4OR2Dic1BItJGUEX+zoXbbi9pCUkV5/A5gk3xAQ==\n=OBcx\n-----END PGP PUBLIC KEY BLOCK-----" }, { - "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwXYDUzQX5WbZ2DUSAcdAL8N/KsIV0/O1DJTQ9ZX2gSet+z4fpHlNGS0md8GNF8++\n7pt+XXGykKLnXDxtbMOQJGRYGbL0svgwpoHwU8eRXgDdcMr4GQwGlAz1GGQqfVyu\nkLwA3PRXR2zMtVQ3EU1+H4zrKcizA49j0sBHAQbGxg1ZU+6nbGXBLYdLs6y4+WcO\nozq45Jucl9cABn8uX9P4hXnncCAnMpMn4DkCPdQ9L+sTLER3WmqLvu/jQ7gMiqgs\nPkvgmt/qtl1NhS6ghnqrRSg3rxAZC9cA2hWV2cmLX9/h7MeiCn/78FIfH5XWOxKW\nMN9zYgzaMZ3M12hbw3/ma/qtXeHc1TeJ7fYGfwDBHIEFUPHtj685XfztcYsPoBi7\nQK4E+RFrpfyzhoU5HYJGsuuQXZIXAQlN6qCM1s9QwVk3zDw4d+H9JJI7YN3FoBrV\n5O9T2aU8Vnnpk6t+sDDZuoW8Igd/OYJkZGspm9ExY0n1mOVEW28NecVtW2nbmcDn\nwLg=\n=A1LE\n-----END PGP MESSAGE--------", - "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", - "name": "curve448", + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwWwD9k8c3Xy6wgEaoEnCrDZ+ywupbVzpxqwY7xi2kBwUS0AkddAksga5gHmf\nxPSQs4qSqG7AvhXuZvUSZe6GxR05oNspCRJd08O05QIL/fmTaF/zS/2mKM3S\nT+zZhnKuAGR+4pkW7014E3zfiKbSwIoBHbZzQeY+L3PxjNN3Fw0rXGvAf7hb\nxUd/7Qi7fN4Q26i+NjJzp7Hfwm8vqHyMRlhoS/ByhEQgfqNvDQtMeWFRW9wf\ndefXCD05zFr4NckvlI6v4FBubRD6AM3yykCHjHjH7l93UoAduL/s3h1URnzT\nzoHqhr5kvrXg6vjrcynTwsQsSfluWGy1ZXhYzA5/kNU4kEweLs4N51gC+eY3\nab0qlsO1vxDcqKlp87Diiug9Jisa0jAUiy+N5CHZwZj1vlM4J++u1AlWvr7q\nortUCQnKISDJdXywfKSF3709Wn0tR+70J2TbVJ/YPp9bgt9iRP7Y39p2MBrB\nUE1CNu2nOpkczvHl4PAyV1g5XHNRWjJ8g83WtYtOaWrYqrnJdyekVf1ut2xa\naLzmmy6yDo++tbGA7lal4VuIWN9MdtFFDaoRFUXHtMsnH1T9+3E=\n=wpjW\n-----END PGP MESSAGE-----\n", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\r\n", + "name": "curve448rfc9580", "password": "", - "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\nQgqsfguR1PqPuJxpXV4bSr6CGAAAAA==\n=MSvh\n-----END PGP PRIVATE KEY BLOCK-----\n", - "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxkYEYV2UmRYDK2VxAc9AFyxgh5xnSbyt50TWl558mw9xdMN+/UBLr5+UMP8IsrvV\nMdXuTIE8CyaUQKSotHtH2RkYEXj5nsMAzRFDdXJ2ZTQ0OCBPcHRpb24gOMLARwQT\nFgoAhgWCYV2UmQWJBaSPvQMLCQcJEKaZmJX6us5SRxQAAAAAAB4AIHNhbHRAbm90\nYXRpb25zLnNlcXVvaWEtcGdwLm9yZw+eVLGDMxOjgAj/poMOc70/gQYKJfP7hUIx\n4OJK41AjAxUKCAKZAQKbAQIeARYhBMHbZdWA17kiclRLHqaZmJX6us5SAAD0CQOX\nQNF6sM5/9LsAqkZbDlQgd3+TTo9XsSp5Qb0cNMU9VQXntcsb5VLeXiAKAsVqEZVg\nSaG9005IZvQBAPHcocD5shn1h8z8OTRRt8bF62XTVzb4rfVjFQD3JrMke7Xl7g5S\nSLcUQHSSXP6nyWFWwRaGja4WAAAAzkYEYV2UmRYDK2VxAc9AS+0x2fTJwi30jydR\nTC4pvXhpENwX5zN4XuazsDt7deECNS4yPejo5HiYqTIHBQJhD9Qav2Rxju6AwsEs\nBBgWCgFrBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0\naW9ucy5zZXF1b2lhLXBncC5vcmcURFIs3U3HqQc4EjDr9lVkAlm5fvtwmRG4AFg6\n8r0rMgKbAsAxoAQZFgoAbwWCYV2UmQkQSqN0i+EuXY5HFAAAAAAAHgAgc2FsdEBu\nb3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnpqBkw45jUIiM8OhzhHXb0tDge1Gfz9O4\nzPxFZp2MU5QWIQQMbJ5BqcdVAoh+oFFKo3SL4S5djgAAC2EDl0ADAdaCqjlsUhZr\n5oAijXGwAMgN6YheRESKrOM9O6Cb6g2Kzlre+VLBCB/9OIFP7x3OBJnxtiQyyYDi\n5ck1D5C/9PH9MnRmT6HxUAYZ/B5LhRY5zbSeaM9xiD+HHVjEBWlMNLr42zzPU5t6\nbRDRnek7NQAAABYhBMHbZdWA17kiclRLHqaZmJX6us5SAABqUwOXQE0eolNhq6VH\nEIKRvianwLBpheMgDoYBcXNVH4nRZN507qd3y4I4qZGGNe9Fi0TW8ipFTkqHcfdx\ngLOfK4sztWnAwFoi6t3MhDwRV28hU4W3DT3C5fv++1JP5oJGKz0BQEFnStZEkZuE\noswwSt+yvAoQAAAAzkkEYV2UmRIDK2VvAcdAzIc74UKPjrF3qkOkFcOLBQmnsR4e\nqy6IYe08y94UEo6Cdu31eiQZCFnZ9C36zJSSA20r9ki3ZYEDAQoJwsA5BBgWCgB4\nBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\nZXF1b2lhLXBncC5vcmcSMExACZz/a7jm7kVAaYFADK2NIbYszclA3489WXm8SQKb\nDBYhBMHbZdWA17kiclRLHqaZmJX6us5SAABJ1gOXQNcQdD3KQ0/atk/fkRPV2aIV\nqAW1OP+EtBxaWDXouq8k0ao6Bhe6pN6tuI463TZcsIGWf7B17ClTgKER98xQNEF+\np9Byj9iwypZXtsckcA7R4L8MwoKPJT3TF0ftH91CCqx+C5HU+o+4nGldXhtKvoIY\nAAAA\n=qcl0\n-----END PGP PUBLIC KEY BLOCK-----" + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nxXsEZwj7fRwTnLec4Csk8IJazjUCMcqOfS+wBFVLk22/qD115v+gBabSgLbv\nhyQV0kBNIoIvwiSAc7/cV+qKkQAATPtPozXndEkNuO2LlPP5L6kbfZ94W0PI\nYKSO4YVygt5zJqRA/gXK2eyfzgAvBUU2oVx1tG8B6FBrHOrNDjx0ZXN0QHRl\nc3QuaXQ+wsBBBBMcCgCFBYJnCPt9AwsJBwmQy11uC0jkkXxFFAAAAAAAHAAg\nc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ6+KGTA4D+iw4TmQeR3FfrkP\ngHTc5C3DyKsQCGgw+WdeBRUICgwOBBYAAgECGQECmwMCHgEWIQQTM/5QoJ0+\nXYOny4TLXW4LSOSRfAAAV3rFKxLtRZ3PgvBXp7uTWzmB7X2r1b2kEVhCBRly\nEjYFmLT8x1wZ56STDLgLw+s1hR+K7PZgkeIUIABPs5UxDODHNXzDQiVlujRY\nmfxScXA8Bb+0tBvAPvfYqZlnappSXL2vSv+LyjvIRQSrks5hOgQZCQDHeQRn\nCPt9GgT1GwtIvDl5Vo4rO2T756NSx6uhOZikiYV5cBKeyfdVjZvbbaDNox2s\nIujUF0ZtEGOsWjwScEyiAEueZy+0sKjhWNI1pltXjAq6yUkp5HYLGu1pfddW\nT+Jc5Z2+n+sLZLgCp+TPcN43Qax1ewWqzcKdHUTCwCwEGBwKAHAFgmcI+30J\nkMtdbgtI5JF8RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v\ncmd3N73b6/Ardzf8LU+uvy6Zk2nA8i53/oRbX29HvfjgvAKbDBYhBBMz/lCg\nnT5dg6fLhMtdbgtI5JF8AACF6bh/9VTD89v8LtHgj7LnXt0xnUYt1AKFCz4u\nG+6HAnf7AKLQhW6F57/wPWOV3ELlFrRHcBC8yrEkgEyBwnbyuLl6Ew38nSsu\nhS8veJU9Ti+I64m7PHIyHpPa2FeSAxw4C2KsVjNRbM6gIRDVviimDaoTAA==\n=ZH8V\n-----END PGP PRIVATE KEY BLOCK-----\n", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxj8EZwj7fRwTnLec4Csk8IJazjUCMcqOfS+wBFVLk22/qD115v+gBabSgLbv\nhyQV0kBNIoIvwiSAc7/cV+qKkQDNDjx0ZXN0QHRlc3QuaXQ+wsBBBBMcCgCF\nBYJnCPt9AwsJBwmQy11uC0jkkXxFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu\nb3BlbnBncGpzLm9yZ6+KGTA4D+iw4TmQeR3FfrkPgHTc5C3DyKsQCGgw+Wde\nBRUICgwOBBYAAgECGQECmwMCHgEWIQQTM/5QoJ0+XYOny4TLXW4LSOSRfAAA\nV3rFKxLtRZ3PgvBXp7uTWzmB7X2r1b2kEVhCBRlyEjYFmLT8x1wZ56STDLgL\nw+s1hR+K7PZgkeIUIABPs5UxDODHNXzDQiVlujRYmfxScXA8Bb+0tBvAPvfY\nqZlnappSXL2vSv+LyjvIRQSrks5hOgQZCQDOPgRnCPt9GgT1GwtIvDl5Vo4r\nO2T756NSx6uhOZikiYV5cBKeyfdVjZvbbaDNox2sIujUF0ZtEGOsWjwScEyi\nwsAsBBgcCgBwBYJnCPt9CZDLXW4LSOSRfEUUAAAAAAAcACBzYWx0QG5vdGF0\naW9ucy5vcGVucGdwanMub3Jndze92+vwK3c3/C1Prr8umZNpwPIud/6EW19v\nR7344LwCmwwWIQQTM/5QoJ0+XYOny4TLXW4LSOSRfAAAhem4f/VUw/Pb/C7R\n4I+y517dMZ1GLdQChQs+LhvuhwJ3+wCi0IVuhee/8D1jldxC5Ra0R3AQvMqx\nJIBMgcJ28ri5ehMN/J0rLoUvL3iVPU4viOuJuzxyMh6T2thXkgMcOAtirFYz\nUWzOoCEQ1b4opg2qEwA=\n=VywH\n-----END PGP PUBLIC KEY BLOCK-----\n" }, { "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwcBOA1N4OCSSjECBEAP8DhX4Ii5TxauisNiJ6ThzZVo0rDrM37eG55Z9/Fp9\nwOFcMoYiM7muadPd0jjVkGk0Y0d1QrfmAW1619L3kv4lGJcB92jEVXeg6HPq\nyLVEc2KzvyIO2ypZ6CBlYhz1iWtc29tgbf1BkVjNGk8C1OIauCqQtNHDwpso\ntFF29gfHKbwEAIkeoyCs85tAyJnNWMrEyMo+GSico4uVEiJCw4DD65O4pW3Y\ns0PUj9HhE8CY01zKADsn9CHo2P0eppbw/7H++ViHdFzkcbrz6Tqt43tC9B29\nNBPdnhMlyJJhivW1FvLoPpuLiYpNb9Dv2lTpug5UUVZR6q9HTuvhP7PJuo5J\n3MIh0qsByqXXlrAZuvZtIZVYX9hFLK7AlLQ4BIbJ5ZoTDOMlamviiKEs/Txj\npBbKbBAQW+fw6ajsKSNoWPqYriVEOGtKCfmrCTe32W0Diifyap7VbsY5q9yK\n07XbMTDZgtxByDMJ9YLdjG2+J9jkQyKoh8SioWZCeRwsUJOjMTVdfbDAeNId\n7me65b7rhtbiR3lU60l5CANdQi+cHTyh3azeFqUqZ5UFNEY8mUXzVWw=\n=+rSf\n-----END PGP MESSAGE-----", diff --git a/openpgp/integration_tests/v2/testdata/test_vectors.json b/openpgp/integration_tests/v2/testdata/test_vectors.json index 1943e624..d5fc0d25 100644 --- a/openpgp/integration_tests/v2/testdata/test_vectors.json +++ b/openpgp/integration_tests/v2/testdata/test_vectors.json @@ -16,12 +16,12 @@ "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxjMEYvTtQBYJKwYBBAHaRw8BAQdAxsNXLbrk5xOjpO24VhOMvQ0/F+JcyIkckMDH\nX3FIGxfNIkdvbGFuZyBHb3BoZXIgPGdvbGFuZ0BleGFtcGxlLm9yZz7CkAQTFggA\nOBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsDBQsJCAcCBhUKCQgLAgQW\nAgMBAh4BAheAAAoJEAy9+ctDgkE3nJ4BAIp2vLwHlJwI+t6/b5QHIwKyztJ27lGG\niYIWwd008twoAQDy6B6L7WjAexX7dwbBZbMqTqwtrxk3zuLdIq44an0NDM44BGL0\n7UASCisGAQQBl1UBBQEBB0DkK5s+nyXuWtVDbmHsfHlc3YLzFqdGeelyPMSigJoQ\nDQMBCAfCeAQYFggAIBYhBIVAcQ5rOaiHWV0gjgy9+ctDgkE3BQJi9O1AAhsMAAoJ\nEAy9+ctDgkE3L3cBAO5W+YP2IrKxH4quutLsDBqHqS1H77ais7kOoXmHvN8tAQDe\nuRX4OR2Dic1BItJGUEX+zoXbbi9pCUkV5/A5gk3xAQ==\n=OBcx\n-----END PGP PUBLIC KEY BLOCK-----" }, { - "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwXYDUzQX5WbZ2DUSAcdAL8N/KsIV0/O1DJTQ9ZX2gSet+z4fpHlNGS0md8GNF8++\n7pt+XXGykKLnXDxtbMOQJGRYGbL0svgwpoHwU8eRXgDdcMr4GQwGlAz1GGQqfVyu\nkLwA3PRXR2zMtVQ3EU1+H4zrKcizA49j0sBHAQbGxg1ZU+6nbGXBLYdLs6y4+WcO\nozq45Jucl9cABn8uX9P4hXnncCAnMpMn4DkCPdQ9L+sTLER3WmqLvu/jQ7gMiqgs\nPkvgmt/qtl1NhS6ghnqrRSg3rxAZC9cA2hWV2cmLX9/h7MeiCn/78FIfH5XWOxKW\nMN9zYgzaMZ3M12hbw3/ma/qtXeHc1TeJ7fYGfwDBHIEFUPHtj685XfztcYsPoBi7\nQK4E+RFrpfyzhoU5HYJGsuuQXZIXAQlN6qCM1s9QwVk3zDw4d+H9JJI7YN3FoBrV\n5O9T2aU8Vnnpk6t+sDDZuoW8Igd/OYJkZGspm9ExY0n1mOVEW28NecVtW2nbmcDn\nwLg=\n=A1LE\n-----END PGP MESSAGE--------", - "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\n", - "name": "curve448", + "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwWwD9k8c3Xy6wgEaoEnCrDZ+ywupbVzpxqwY7xi2kBwUS0AkddAksga5gHmf\nxPSQs4qSqG7AvhXuZvUSZe6GxR05oNspCRJd08O05QIL/fmTaF/zS/2mKM3S\nT+zZhnKuAGR+4pkW7014E3zfiKbSwIoBHbZzQeY+L3PxjNN3Fw0rXGvAf7hb\nxUd/7Qi7fN4Q26i+NjJzp7Hfwm8vqHyMRlhoS/ByhEQgfqNvDQtMeWFRW9wf\ndefXCD05zFr4NckvlI6v4FBubRD6AM3yykCHjHjH7l93UoAduL/s3h1URnzT\nzoHqhr5kvrXg6vjrcynTwsQsSfluWGy1ZXhYzA5/kNU4kEweLs4N51gC+eY3\nab0qlsO1vxDcqKlp87Diiug9Jisa0jAUiy+N5CHZwZj1vlM4J++u1AlWvr7q\nortUCQnKISDJdXywfKSF3709Wn0tR+70J2TbVJ/YPp9bgt9iRP7Y39p2MBrB\nUE1CNu2nOpkczvHl4PAyV1g5XHNRWjJ8g83WtYtOaWrYqrnJdyekVf1ut2xa\naLzmmy6yDo++tbGA7lal4VuIWN9MdtFFDaoRFUXHtMsnH1T9+3E=\n=wpjW\n-----END PGP MESSAGE-----\n", + "message": "test\u554f\u91cf\u9bae\u63a7\u5230\u6848\u9032\u5e73\r\n", + "name": "curve448rfc9580", "password": "", - "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n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\nQgqsfguR1PqPuJxpXV4bSr6CGAAAAA==\n=MSvh\n-----END PGP PRIVATE KEY BLOCK-----\n", - "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxkYEYV2UmRYDK2VxAc9AFyxgh5xnSbyt50TWl558mw9xdMN+/UBLr5+UMP8IsrvV\nMdXuTIE8CyaUQKSotHtH2RkYEXj5nsMAzRFDdXJ2ZTQ0OCBPcHRpb24gOMLARwQT\nFgoAhgWCYV2UmQWJBaSPvQMLCQcJEKaZmJX6us5SRxQAAAAAAB4AIHNhbHRAbm90\nYXRpb25zLnNlcXVvaWEtcGdwLm9yZw+eVLGDMxOjgAj/poMOc70/gQYKJfP7hUIx\n4OJK41AjAxUKCAKZAQKbAQIeARYhBMHbZdWA17kiclRLHqaZmJX6us5SAAD0CQOX\nQNF6sM5/9LsAqkZbDlQgd3+TTo9XsSp5Qb0cNMU9VQXntcsb5VLeXiAKAsVqEZVg\nSaG9005IZvQBAPHcocD5shn1h8z8OTRRt8bF62XTVzb4rfVjFQD3JrMke7Xl7g5S\nSLcUQHSSXP6nyWFWwRaGja4WAAAAzkYEYV2UmRYDK2VxAc9AS+0x2fTJwi30jydR\nTC4pvXhpENwX5zN4XuazsDt7deECNS4yPejo5HiYqTIHBQJhD9Qav2Rxju6AwsEs\nBBgWCgFrBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0\naW9ucy5zZXF1b2lhLXBncC5vcmcURFIs3U3HqQc4EjDr9lVkAlm5fvtwmRG4AFg6\n8r0rMgKbAsAxoAQZFgoAbwWCYV2UmQkQSqN0i+EuXY5HFAAAAAAAHgAgc2FsdEBu\nb3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnpqBkw45jUIiM8OhzhHXb0tDge1Gfz9O4\nzPxFZp2MU5QWIQQMbJ5BqcdVAoh+oFFKo3SL4S5djgAAC2EDl0ADAdaCqjlsUhZr\n5oAijXGwAMgN6YheRESKrOM9O6Cb6g2Kzlre+VLBCB/9OIFP7x3OBJnxtiQyyYDi\n5ck1D5C/9PH9MnRmT6HxUAYZ/B5LhRY5zbSeaM9xiD+HHVjEBWlMNLr42zzPU5t6\nbRDRnek7NQAAABYhBMHbZdWA17kiclRLHqaZmJX6us5SAABqUwOXQE0eolNhq6VH\nEIKRvianwLBpheMgDoYBcXNVH4nRZN507qd3y4I4qZGGNe9Fi0TW8ipFTkqHcfdx\ngLOfK4sztWnAwFoi6t3MhDwRV28hU4W3DT3C5fv++1JP5oJGKz0BQEFnStZEkZuE\noswwSt+yvAoQAAAAzkkEYV2UmRIDK2VvAcdAzIc74UKPjrF3qkOkFcOLBQmnsR4e\nqy6IYe08y94UEo6Cdu31eiQZCFnZ9C36zJSSA20r9ki3ZYEDAQoJwsA5BBgWCgB4\nBYJhXZSZBYkFpI+9CRCmmZiV+rrOUkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\nZXF1b2lhLXBncC5vcmcSMExACZz/a7jm7kVAaYFADK2NIbYszclA3489WXm8SQKb\nDBYhBMHbZdWA17kiclRLHqaZmJX6us5SAABJ1gOXQNcQdD3KQ0/atk/fkRPV2aIV\nqAW1OP+EtBxaWDXouq8k0ao6Bhe6pN6tuI463TZcsIGWf7B17ClTgKER98xQNEF+\np9Byj9iwypZXtsckcA7R4L8MwoKPJT3TF0ftH91CCqx+C5HU+o+4nGldXhtKvoIY\nAAAA\n=qcl0\n-----END PGP PUBLIC KEY BLOCK-----" + "privateKey": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nxXsEZwj7fRwTnLec4Csk8IJazjUCMcqOfS+wBFVLk22/qD115v+gBabSgLbv\nhyQV0kBNIoIvwiSAc7/cV+qKkQAATPtPozXndEkNuO2LlPP5L6kbfZ94W0PI\nYKSO4YVygt5zJqRA/gXK2eyfzgAvBUU2oVx1tG8B6FBrHOrNDjx0ZXN0QHRl\nc3QuaXQ+wsBBBBMcCgCFBYJnCPt9AwsJBwmQy11uC0jkkXxFFAAAAAAAHAAg\nc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ6+KGTA4D+iw4TmQeR3FfrkP\ngHTc5C3DyKsQCGgw+WdeBRUICgwOBBYAAgECGQECmwMCHgEWIQQTM/5QoJ0+\nXYOny4TLXW4LSOSRfAAAV3rFKxLtRZ3PgvBXp7uTWzmB7X2r1b2kEVhCBRly\nEjYFmLT8x1wZ56STDLgLw+s1hR+K7PZgkeIUIABPs5UxDODHNXzDQiVlujRY\nmfxScXA8Bb+0tBvAPvfYqZlnappSXL2vSv+LyjvIRQSrks5hOgQZCQDHeQRn\nCPt9GgT1GwtIvDl5Vo4rO2T756NSx6uhOZikiYV5cBKeyfdVjZvbbaDNox2s\nIujUF0ZtEGOsWjwScEyiAEueZy+0sKjhWNI1pltXjAq6yUkp5HYLGu1pfddW\nT+Jc5Z2+n+sLZLgCp+TPcN43Qax1ewWqzcKdHUTCwCwEGBwKAHAFgmcI+30J\nkMtdbgtI5JF8RRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v\ncmd3N73b6/Ardzf8LU+uvy6Zk2nA8i53/oRbX29HvfjgvAKbDBYhBBMz/lCg\nnT5dg6fLhMtdbgtI5JF8AACF6bh/9VTD89v8LtHgj7LnXt0xnUYt1AKFCz4u\nG+6HAnf7AKLQhW6F57/wPWOV3ELlFrRHcBC8yrEkgEyBwnbyuLl6Ew38nSsu\nhS8veJU9Ti+I64m7PHIyHpPa2FeSAxw4C2KsVjNRbM6gIRDVviimDaoTAA==\n=ZH8V\n-----END PGP PRIVATE KEY BLOCK-----\n", + "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxj8EZwj7fRwTnLec4Csk8IJazjUCMcqOfS+wBFVLk22/qD115v+gBabSgLbv\nhyQV0kBNIoIvwiSAc7/cV+qKkQDNDjx0ZXN0QHRlc3QuaXQ+wsBBBBMcCgCF\nBYJnCPt9AwsJBwmQy11uC0jkkXxFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMu\nb3BlbnBncGpzLm9yZ6+KGTA4D+iw4TmQeR3FfrkPgHTc5C3DyKsQCGgw+Wde\nBRUICgwOBBYAAgECGQECmwMCHgEWIQQTM/5QoJ0+XYOny4TLXW4LSOSRfAAA\nV3rFKxLtRZ3PgvBXp7uTWzmB7X2r1b2kEVhCBRlyEjYFmLT8x1wZ56STDLgL\nw+s1hR+K7PZgkeIUIABPs5UxDODHNXzDQiVlujRYmfxScXA8Bb+0tBvAPvfY\nqZlnappSXL2vSv+LyjvIRQSrks5hOgQZCQDOPgRnCPt9GgT1GwtIvDl5Vo4r\nO2T756NSx6uhOZikiYV5cBKeyfdVjZvbbaDNox2sIujUF0ZtEGOsWjwScEyi\nwsAsBBgcCgBwBYJnCPt9CZDLXW4LSOSRfEUUAAAAAAAcACBzYWx0QG5vdGF0\naW9ucy5vcGVucGdwanMub3Jndze92+vwK3c3/C1Prr8umZNpwPIud/6EW19v\nR7344LwCmwwWIQQTM/5QoJ0+XYOny4TLXW4LSOSRfAAAhem4f/VUw/Pb/C7R\n4I+y517dMZ1GLdQChQs+LhvuhwJ3+wCi0IVuhee/8D1jldxC5Ra0R3AQvMqx\nJIBMgcJ28ri5ehMN/J0rLoUvL3iVPU4viOuJuzxyMh6T2thXkgMcOAtirFYz\nUWzOoCEQ1b4opg2qEwA=\n=VywH\n-----END PGP PUBLIC KEY BLOCK-----\n" }, { "encryptedSignedMessage": "-----BEGIN PGP MESSAGE-----\n\nwcBOA1N4OCSSjECBEAP8DhX4Ii5TxauisNiJ6ThzZVo0rDrM37eG55Z9/Fp9\nwOFcMoYiM7muadPd0jjVkGk0Y0d1QrfmAW1619L3kv4lGJcB92jEVXeg6HPq\nyLVEc2KzvyIO2ypZ6CBlYhz1iWtc29tgbf1BkVjNGk8C1OIauCqQtNHDwpso\ntFF29gfHKbwEAIkeoyCs85tAyJnNWMrEyMo+GSico4uVEiJCw4DD65O4pW3Y\ns0PUj9HhE8CY01zKADsn9CHo2P0eppbw/7H++ViHdFzkcbrz6Tqt43tC9B29\nNBPdnhMlyJJhivW1FvLoPpuLiYpNb9Dv2lTpug5UUVZR6q9HTuvhP7PJuo5J\n3MIh0qsByqXXlrAZuvZtIZVYX9hFLK7AlLQ4BIbJ5ZoTDOMlamviiKEs/Txj\npBbKbBAQW+fw6ajsKSNoWPqYriVEOGtKCfmrCTe32W0Diifyap7VbsY5q9yK\n07XbMTDZgtxByDMJ9YLdjG2+J9jkQyKoh8SioWZCeRwsUJOjMTVdfbDAeNId\n7me65b7rhtbiR3lU60l5CANdQi+cHTyh3azeFqUqZ5UFNEY8mUXzVWw=\n=+rSf\n-----END PGP MESSAGE-----", From 384a0e0ac488f46c0a293ebf9d17952f16dd8747 Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 11 Oct 2024 11:48:17 +0200 Subject: [PATCH 35/36] chore: Add kmac back --- internal/kmac/kmac.go | 145 +++++++++++++++++++++++++++++++++++++ internal/kmac/kmac_test.go | 133 ++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 internal/kmac/kmac.go create mode 100644 internal/kmac/kmac_test.go diff --git a/internal/kmac/kmac.go b/internal/kmac/kmac.go new file mode 100644 index 00000000..982ca409 --- /dev/null +++ b/internal/kmac/kmac.go @@ -0,0 +1,145 @@ +// 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" + "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) hash.Hash { + if len(key) < 16 { + panic("Key must not be smaller than security strength") + } + c := sha3.NewCShake128([]byte("KMAC"), customizationString) + return newKMAC(key, tagSize, c, rate128) +} + +// 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) hash.Hash { + if len(key) < 32 { + panic("Key must not be smaller than security strength") + } + c := sha3.NewCShake256([]byte("KMAC"), customizationString) + return newKMAC(key, tagSize, c, rate256) +} + +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 00000000..0b484425 --- /dev/null +++ b/internal/kmac/kmac_test.go @@ -0,0 +1,133 @@ +/// 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 = kmac.NewKMAC128(key, len(tag), []byte(test.customization)) + } else { + mac = kmac.NewKMAC256(key, len(tag), []byte(test.customization)) + } + 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 := kmac.NewKMAC256(key, len(tag), []byte("Partition1")) + k.Write(msg) + k.Sum(tag[:0]) + fmt.Println(hex.EncodeToString(tag)) + // Example 2: Different customization string produces different digest + k = kmac.NewKMAC256(key, 16, []byte("Partition2")) + k.Write(msg) + k.Sum(tag[:0]) + fmt.Println(hex.EncodeToString(tag)) + // Output: + //3814d78758add078334b8ab9e5c4f942 + //3762371e99e1e01ab17742b95c0360da +} From 86c81cbd9479a1fddcab90af8642a009d74e615c Mon Sep 17 00:00:00 2001 From: Lukas Burkhalter Date: Fri, 11 Oct 2024 15:28:13 +0200 Subject: [PATCH 36/36] feat: Update to new kmac key combiner in kem See: https://github.com/openpgp-pqc/draft-openpgp-pqc/pull/138 --- openpgp/mlkem_ecdh/mlkem_ecdh.go | 55 ++++++++++++++++++++------------ openpgp/v2/read_test.go | 5 +-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go index cb007d20..c532d629 100644 --- a/openpgp/mlkem_ecdh/mlkem_ecdh.go +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -3,10 +3,12 @@ package mlkem_ecdh import ( + "bytes" goerrors "errors" "fmt" "io" + "github.com/ProtonMail/go-crypto/internal/kmac" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" "golang.org/x/crypto/sha3" @@ -18,7 +20,7 @@ import ( const ( maxSessionKeyLength = 64 - kdfContext = "OpenPGPCompositeKDFv1" + domainSeparator = "OpenPGPCompositeKDFv1" ) type PublicKey struct { @@ -122,7 +124,7 @@ func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg } // buildKey implements the composite KDF as specified in -// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-key-combiner +// 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() @@ -132,28 +134,41 @@ func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemK _, _ = h.Write(eccPublicKey) eccKeyShare := h.Sum(nil) - serializedMlkemKey, err := mlkemPublicKey.MarshalBinary() + serializedMlkemPublicKey, err := mlkemPublicKey.MarshalBinary() if err != nil { return nil, err } - // eccData = eccKeyShare || eccCipherText - // mlkemData = mlkemKeyShare || mlkemCipherText - // encData = counter || eccData || mlkemData || fixedInfo - h.Reset() - - // SHA3 never returns error - _, _ = h.Write([]byte{0x00, 0x00, 0x00, 0x01}) - _, _ = h.Write(eccKeyShare) - _, _ = h.Write(eccEphemeral) - _, _ = h.Write(eccPublicKey) - _, _ = h.Write(mlkemKeyShare) - _, _ = h.Write(mlkemEphemeral) - _, _ = h.Write(serializedMlkemKey) - _, _ = h.Write([]byte{pub.AlgId}) - _, _ = h.Write([]byte(kdfContext)) - - return h.Sum(nil), nil + // 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 + // 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" + + // KEK = KMAC256( + // eccKeyShare || mlkemKeyShare, + // eccEphemeral || mlkemEphemeral || ecdhPublicKey || mlkemPublicKey || algId, + // 256 (32 bytes), + // domainSeparator + // ) + + kMacKeyBuffer := bytes.NewBuffer(make([]byte, len(eccKeyShare)+len(mlkemKeyShare))) + _, _ = kMacKeyBuffer.Write(eccKeyShare) + _, _ = kMacKeyBuffer.Write(mlkemKeyShare) + + k := kmac.NewKMAC256(kMacKeyBuffer.Bytes(), 32, []byte(domainSeparator)) + + // kmac hash never returns an error + _, _ = k.Write(eccEphemeral) + _, _ = k.Write(mlkemEphemeral) + _, _ = k.Write(eccPublicKey) + _, _ = k.Write(serializedMlkemPublicKey) + _, _ = k.Write([]byte{pub.AlgId}) + + return k.Sum(nil), nil } // Validate checks that the public key corresponds to the private key diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index ac140049..8b94d023 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -1018,7 +1018,8 @@ var pqcDraftVectors = map[string]struct { armoredMessages []string v6 bool }{ - "v4_Ed25519_ML-KEM-768+X25519": { + // TODO: Update with fresh test vectors + /*"v4_Ed25519_ML-KEM-768+X25519": { v4Ed25519Mlkem768X25519PrivateTestVector, v4Ed25519Mlkem768X25519PublicTestVector, []string{"b2e9b532d55bd6287ec79e17c62adc0ddd1edd73", "95bed3c63f295e7b980b6a2b93b3233faf28c9d2", "bd67d98388813e88bf3490f3e440cfbaffd6f357"}, @@ -1031,7 +1032,7 @@ var pqcDraftVectors = map[string]struct { []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, true, - }, + },*/ } func TestPqcDraftVectors(t *testing.T) {