diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1a05c919..c5bade07 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, Proton ] jobs: @@ -26,22 +26,3 @@ jobs: - name: Randomized test suite 2 run: go test -v ./... -run RandomizeSlow -count=32 - - test-old: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Go 1.17 - uses: actions/setup-go@v3 - with: - go-version: 1.17 - - - name: Short test - run: go test -short -v ./... - - - name: Randomized test suite 1 - run: go test -v ./... -run RandomizeFast -count=512 - - - name: Randomized test suite 2 - run: go test -v ./... -run RandomizeSlow -count=32 \ No newline at end of file diff --git a/go.mod b/go.mod index d417da35..25880a80 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/ProtonMail/go-crypto -go 1.17 +go 1.22.0 require ( - github.com/cloudflare/circl v1.3.7 - golang.org/x/crypto v0.17.0 + github.com/cloudflare/circl v1.5.0 + golang.org/x/crypto v0.25.0 ) -require golang.org/x/sys v0.16.0 // indirect +require golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index 712b2d44..1a97c0f3 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,6 @@ -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= +github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/kmac/kmac.go b/internal/kmac/kmac.go new file mode 100644 index 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 +} diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go new file mode 100644 index 00000000..c0593765 --- /dev/null +++ b/openpgp/benchmark_v6_test.go @@ -0,0 +1,295 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + "io/ioutil" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const benchmarkMessageSize = 1024 // Signed / encrypted message size in bytes + +var benchmarkTestSet = map[string]*packet.Config{ + "RSA_1024": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 1024, + }, + "RSA_2048": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 2048, + }, + "RSA_3072": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 3072, + }, + "RSA_4096": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 4096, + }, + "Ed25519_X25519": { + Algorithm: packet.PubKeyAlgoEd25519, + }, + "Ed448_X448": { + Algorithm: packet.PubKeyAlgoEd448, + }, + "P256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP256, + }, + "P384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP384, + }, + "P521": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP521, + }, + "Brainpool256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP256, + }, + "Brainpool384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP384, + }, + "Brainpool512": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP512, + }, + "ML-DSA3Ed25519_ML-KEM768X25519": { + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + }, + "ML-DSA5Ed448_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + }, +} + +func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { + var serializedEntities [][]byte + config.V6Keys = true + + config.AEADConfig = &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + } + + config.Time = func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + b.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + b.Fatalf("Failed to serialize entity: %s", err) + } + + serializedEntities = append(serializedEntities, serializedEntity.Bytes()) + } + + return serializedEntities +} + +func benchmarkParse(b *testing.B, keys [][]byte) []*Entity { + var parsedKeys []*Entity + + b.ResetTimer() + for n := 0; n < b.N; n++ { + keyring, err := ReadKeyRing(bytes.NewReader(keys[n])) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + parsedKeys = append(parsedKeys, keyring[0]) + } + + return parsedKeys +} + +func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) [][]byte { + var encryptedMessages [][]byte + + var config = &packet.Config{ + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + V6Keys: true, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + var signed *Entity + if sign { + signed = keys[n%len(keys)] + } + + w, err := Encrypt(buf, EntityList{keys[n%len(keys)]}, signed, nil, config) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + _, err = w.Write(plaintext) + if err != nil { + b.Errorf("Error writing plaintext: %s", err) + continue + } + + err = w.Close() + if err != nil { + b.Errorf("Error closing WriteCloser: %s", err) + continue + } + + encryptedMessages = append(encryptedMessages, buf.Bytes()) + } + + return encryptedMessages +} + +func benchmarkDecrypt(b *testing.B, keys []*Entity, plaintext []byte, encryptedMessages [][]byte, verify bool) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + reader := bytes.NewReader(encryptedMessages[n%len(encryptedMessages)]) + md, err := ReadMessage(reader, EntityList{keys[n%len(keys)]}, nil, nil) + if err != nil { + b.Errorf("Error reading message: %s", err) + continue + } + + decrypted, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + b.Errorf("Error reading encrypted content: %s", err) + continue + } + + if !bytes.Equal(decrypted, plaintext) { + b.Error("Decrypted wrong plaintext") + } + + if verify { + if md.SignatureError != nil { + b.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + b.Error("Signature missing") + } + } + } +} + +func benchmarkSign(b *testing.B, keys []*Entity, plaintext []byte) [][]byte { + var signatures [][]byte + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + err := DetachSign(buf, keys[n%len(keys)], bytes.NewReader(plaintext), nil) + if err != nil { + b.Errorf("Failed to sign: %s", err) + continue + } + + signatures = append(signatures, buf.Bytes()) + } + + return signatures +} + +func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures [][]byte) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + signed := bytes.NewReader(plaintext) + signature := bytes.NewReader(signatures[n%len(signatures)]) + + parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n%len(keys)]}, signed, signature, nil) + + if signatureError != nil { + b.Errorf("Signature error: %s", signatureError) + } + + if parsedSignature == nil { + b.Error("Signature missing") + } + + if signer == nil { + b.Error("Signer missing") + } + } +} + +func BenchmarkV6Keys(b *testing.B) { + serializedKeys := make(map[string][][]byte) + parsedKeys := make(map[string][]*Entity) + encryptedMessages := make(map[string][][]byte) + encryptedSignedMessages := make(map[string][][]byte) + signatures := make(map[string][][]byte) + + var plaintext [benchmarkMessageSize]byte + _, _ = rand.Read(plaintext[:]) + + for name, config := range benchmarkTestSet { + b.Run("Generate "+name, func(b *testing.B) { + serializedKeys[name] = benchmarkGenerateKey(b, config) + b.Logf("Generate %s: %d bytes", name, len(serializedKeys[name][0])) + }) + } + + for name, keys := range serializedKeys { + b.Run("Parse_"+name, func(b *testing.B) { + parsedKeys[name] = benchmarkParse(b, keys) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_"+name, func(b *testing.B) { + encryptedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], false) + b.Logf("Encrypt %s: %d bytes", name, len(encryptedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedMessages[name], false) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_Sign_"+name, func(b *testing.B) { + encryptedSignedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], true) + b.Logf("Encrypt_Sign %s: %d bytes", name, len(encryptedSignedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_Verify_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedSignedMessages[name], true) + }) + } + + for name, keys := range parsedKeys { + b.Run("Sign_"+name, func(b *testing.B) { + signatures[name] = benchmarkSign(b, keys, plaintext[:]) + b.Logf("Sign %s: %d bytes", name, len(signatures[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Verify_"+name, func(b *testing.B) { + benchmarkVerify(b, keys, plaintext[:], signatures[name]) + }) + } +} diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index ae3403e9..85a06b17 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -12,13 +12,50 @@ import ( "io" "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519" +) + +const ( + KDFVersion1 = 1 + KDFVersionForwarding = 255 ) type KDF struct { - Hash algorithm.Hash - Cipher algorithm.Cipher + Version int // Defaults to v1; 255 for forwarding + Hash algorithm.Hash + Cipher algorithm.Cipher + ReplacementFingerprint []byte // (forwarding only) fingerprint to use instead of recipient's (20 octets) +} + +func (kdf *KDF) Serialize(w io.Writer) (err error) { + switch kdf.Version { + case 0, KDFVersion1: // Default to v1 if unspecified + return kdf.serializeForHash(w) + case KDFVersionForwarding: + // Length || Version || Hash || Cipher || Replacement Fingerprint + length := byte(3 + len(kdf.ReplacementFingerprint)) + if _, err := w.Write([]byte{length, KDFVersionForwarding, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + if _, err := w.Write(kdf.ReplacementFingerprint); err != nil { + return err + } + + return nil + default: + return errors.New("ecdh: invalid KDF version") + } +} + +func (kdf *KDF) serializeForHash(w io.Writer) (err error) { + // Length || Version || Hash || Cipher + if _, err := w.Write([]byte{3, KDFVersion1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + return nil } type PublicKey struct { @@ -32,13 +69,10 @@ type PrivateKey struct { D []byte } -func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey { +func NewPublicKey(curve ecc.ECDHCurve, kdf KDF) *PublicKey { return &PublicKey{ curve: curve, - KDF: KDF{ - Hash: kdfHash, - Cipher: kdfCipher, - }, + KDF: kdf, } } @@ -149,25 +183,32 @@ func Decrypt(priv *PrivateKey, vsG, c, curveOID, fingerprint []byte) (msg []byte } func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLeading, stripTrailing bool) ([]byte, error) { - // Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 - // || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap + // Param = curve_OID_len || curve_OID || public_key_alg_ID + // || KDF_params for AESKeyWrap // || "Anonymous Sender " || recipient_fingerprint; param := new(bytes.Buffer) if _, err := param.Write(curveOID); err != nil { return nil, err } - algKDF := []byte{18, 3, 1, pub.KDF.Hash.Id(), pub.KDF.Cipher.Id()} - if _, err := param.Write(algKDF); err != nil { + algo := []byte{18} + if _, err := param.Write(algo); err != nil { return nil, err } - if _, err := param.Write([]byte("Anonymous Sender ")); err != nil { + + if err := pub.KDF.serializeForHash(param); err != nil { return nil, err } - if _, err := param.Write(fingerprint[:]); err != nil { + + if _, err := param.Write([]byte("Anonymous Sender ")); err != nil { return nil, err } - if param.Len()-len(curveOID) != 45 { - return nil, errors.New("ecdh: malformed KDF Param") + + if pub.KDF.ReplacementFingerprint != nil { + fingerprint = pub.KDF.ReplacementFingerprint + } + + if _, err := param.Write(fingerprint); err != nil { + return nil, err } // MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param ); @@ -207,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 1f70b7dd..0170d776 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -88,7 +88,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { p := priv.MarshalPoint() d := priv.MarshalByteSecret() - parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF.Hash, priv.KDF.Cipher)) + parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF)) if err := parsed.UnmarshalPoint(p); err != nil { t.Fatalf("unable to unmarshal point: %s", err) @@ -112,3 +112,37 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { t.Fatal("failed to marshal/unmarshal correctly") } } + +func TestKDFParamsWrite(t *testing.T) { + kdf := KDF{ + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + } + byteBuffer := new(bytes.Buffer) + + testFingerprint := make([]byte, 20) + + expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()} + kdf.Serialize(byteBuffer) + gotBytes := byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV1) { + t.Errorf("error serializing KDF params, got %x, want: %x", gotBytes, expectBytesV1) + } + byteBuffer.Reset() + + kdfV2 := KDF{ + Version: KDFVersionForwarding, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + ReplacementFingerprint: testFingerprint, + } + expectBytesV2 := []byte{23, 0xFF, kdfV2.Hash.Id(), kdfV2.Cipher.Id()} + expectBytesV2 = append(expectBytesV2, testFingerprint...) + + kdfV2.Serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2) { + t.Errorf("error serializing KDF params v2, got %x, want: %x", gotBytes, expectBytesV2) + } + byteBuffer.Reset() +} diff --git a/openpgp/errors/errors.go b/openpgp/errors/errors.go index 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..ae45c3c2 --- /dev/null +++ b/openpgp/forwarding.go @@ -0,0 +1,163 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openpgp + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + i := e.PrimaryIdentity() + if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired + i.SelfSignature.SigExpired(now) || // user ID self-signature has expired + e.Revoked(now) || // primary key has been revoked + i.Revoked(now) { // user ID has been revoked + return nil, nil, errors.InvalidArgumentError("primary key is expired") + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + keyLifetimeSecs := config.KeyLifetime() + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs, true) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + // Filter expiration & revokal + if forwarderSubKey.PublicKey.KeyExpired(forwarderSubKey.Sig, now) || + forwarderSubKey.Sig.SigExpired(now) || + forwarderSubKey.Revoked(now) { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKey.Sig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKey.Sig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKey.Sig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKey.Sig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKey.Sig.FlagForward = true + + // Re-sign subkey binding signature + err = forwardeeSubKey.Sig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go new file mode 100644 index 00000000..7bc16718 --- /dev/null +++ b/openpgp/forwarding_test.go @@ -0,0 +1,224 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "golang.org/x/crypto/openpgp/armor" +) + +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj +-----END PGP PRIVATE KEY BLOCK-----` + +const forwardedMessage = `-----BEGIN PGP MESSAGE----- + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa +-----END PGP MESSAGE-----` + +const forwardedPlaintext = "Message for Bob" + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) + if err != nil { + t.Error(err) + return + } + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) + if err != nil { + t.Error(err) + return + } + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) + if err != nil { + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingFull(t *testing.T) { + keyConfig := &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + Curve: packet.Curve25519, + } + + plaintext := make([]byte, 1024) + rand.Read(plaintext) + + bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig) + if err != nil { + t.Fatal(err) + } + + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) + } + + if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + _, err = w.Write(plaintext) + if err != nil { + t.Fatal(err) + } + + err = w.Close() + if err != nil { + t.Fatal(err) + } + + encrypted := buf.Bytes() + + // Decrypt message for Bob + m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("decrypted does not match original") + } + + // Forward message + + transformed := transformTestMessage(t, encrypted, instances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } + + // Setup further forwarding + danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { + bytesReader := bytes.NewReader(encrypted) + packets := packet.NewReader(bytesReader) + splitPoint := int64(0) + transformedEncryptedKey := bytes.NewBuffer(nil) + +Loop: + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error in parsing message: %s", err) + } + switch p := p.(type) { + case *packet.EncryptedKey: + tp, err := p.ProxyTransform(instance) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = tp.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + return transformed +} + +func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity { + serializedEntity := bytes.NewBuffer(nil) + err := key.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatalf("Error in serializing forwardee key: %s", err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatalf("Error in reading forwardee key: %s", err) + } + + if len(el) != 1 { + t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el)) + } + + return el[0] +} diff --git a/openpgp/integration_tests/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-----", 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/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..21670a82 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519.go @@ -0,0 +1,122 @@ +// Package curve25519 implements custom field operations without clamping for forwarding. +package curve25519 + +import ( + "crypto/subtle" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519/field" + x25519lib "github.com/cloudflare/circl/dh/x25519" + "math/big" +) + +var curveGroupByte = [x25519lib.Size]byte{ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, +} + +const ParamSize = x25519lib.Size + +func DeriveProxyParam(recipientSecretByte, forwardeeSecretByte []byte) (proxyParam []byte, err error) { + curveGroup := new(big.Int).SetBytes(curveGroupByte[:]) + recipientSecret := new(big.Int).SetBytes(recipientSecretByte) + forwardeeSecret := new(big.Int).SetBytes(forwardeeSecretByte) + + proxyTransform := new(big.Int).Mod( + new(big.Int).Mul( + new(big.Int).ModInverse(forwardeeSecret, curveGroup), + recipientSecret, + ), + curveGroup, + ) + + rawProxyParam := proxyTransform.Bytes() + + // pad and convert to small endian + proxyParam = make([]byte, x25519lib.Size) + l := len(rawProxyParam) + for i := 0; i < l; i++ { + proxyParam[i] = rawProxyParam[l-i-1] + } + + return proxyParam, nil +} + +func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) { + var transformed, safetyCheck [x25519lib.Size]byte + + var scalarEight = make([]byte, x25519lib.Size) + scalarEight[0] = 0x08 + err := ScalarMult(&safetyCheck, scalarEight, ephemeral) + if err != nil { + return nil, err + } + + err = ScalarMult(&transformed, proxyParam, ephemeral) + if err != nil { + return nil, err + } + + return transformed[:], nil +} + +func ScalarMult(dst *[32]byte, scalar, point []byte) error { + var in, base, zero [32]byte + copy(in[:], scalar) + copy(base[:], point) + + scalarMult(dst, &in, &base) + if subtle.ConstantTimeCompare(dst[:], zero[:]) == 1 { + return errors.InvalidArgumentError("invalid ephemeral: low order point") + } + + return nil +} + +func scalarMult(dst, scalar, point *[32]byte) { + var e [32]byte + + copy(e[:], scalar[:]) + + var x1, x2, z2, x3, z3, tmp0, tmp1 field.Element + x1.SetBytes(point[:]) + x2.One() + x3.Set(&x1) + z3.One() + + swap := 0 + for pos := 254; pos >= 0; pos-- { + b := e[pos/8] >> uint(pos&7) + b &= 1 + swap ^= int(b) + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + swap = int(b) + + tmp0.Subtract(&x3, &z3) + tmp1.Subtract(&x2, &z2) + x2.Add(&x2, &z2) + z2.Add(&x3, &z3) + z3.Multiply(&tmp0, &x2) + z2.Multiply(&z2, &tmp1) + tmp0.Square(&tmp1) + tmp1.Square(&x2) + x3.Add(&z3, &z2) + z2.Subtract(&z3, &z2) + x2.Multiply(&tmp1, &tmp0) + tmp1.Subtract(&tmp1, &tmp0) + z2.Square(&z2) + + z3.Mult32(&tmp1, 121666) + x3.Square(&x3) + tmp0.Add(&tmp0, &z3) + z3.Multiply(&x1, &z2) + z2.Multiply(&tmp1, &tmp0) + } + + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + + z2.Invert(&z2) + x2.Multiply(&x2, &z2) + copy(dst[:], x2.Bytes()) +} diff --git a/openpgp/internal/ecc/curve25519/curve25519_test.go b/openpgp/internal/ecc/curve25519/curve25519_test.go new file mode 100644 index 00000000..bd82e03e --- /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..64e57c4f --- /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..edcf163c --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.go @@ -0,0 +1,16 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +//go:build amd64 && gc && !purego +// +build amd64,gc,!purego + +package field + +// feMul sets out = a * b. It works like feMulGeneric. +// +//go:noescape +func feMul(out *Element, a *Element, b *Element) + +// feSquare sets out = a * a. It works like feSquareGeneric. +// +//go:noescape +func feSquare(out *Element, a *Element) diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.s b/openpgp/internal/ecc/curve25519/field/fe_amd64.s new file mode 100644 index 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/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..44fad3b4 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..e5e4a827 --- /dev/null +++ b/openpgp/internal/encoding/octetarray.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package encoding + +import ( + "io" +) + +// OctetArray is used to store a fixed-length field +type OctetArray struct { + length int + bytes []byte +} + +// NewOctetArray returns a OID initialized with bytes. +func NewOctetArray(bytes []byte) *OctetArray { + return &OctetArray{ + length: len(bytes), + bytes: bytes, + } +} + +func NewEmptyOctetArray(length int) *OctetArray { + return &OctetArray{ + length: length, + bytes: nil, + } +} + +// Bytes returns the decoded data. +func (o *OctetArray) Bytes() []byte { + return o.bytes +} + +// BitLength is the size in bits of the decoded data. +func (o *OctetArray) BitLength() uint16 { + return uint16(o.length * 8) +} + +// EncodedBytes returns the encoded data. +func (o *OctetArray) EncodedBytes() []byte { + if len(o.bytes) != o.length { + panic("invalid length") + } + return o.bytes +} + +// EncodedLength is the size in bytes of the encoded data. +func (o *OctetArray) EncodedLength() uint16 { + return uint16(o.length) +} + +// ReadFrom reads into b the next OID from r. +func (o *OctetArray) ReadFrom(r io.Reader) (int64, error) { + o.bytes = make([]byte, o.length) + + nn, err := io.ReadFull(r, o.bytes) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + return int64(nn), err +} diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go new file mode 100644 index 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..37510a35 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var octetStreamTests = []struct { + data []byte +}{ + { + data: []byte{0x0, 0x0, 0x0}, + }, + { + data: []byte{0x1, 0x2, 0x03}, + }, + { + data: make([]byte, 255), + }, +} + +func TestShortByteString(t *testing.T) { + for i, test := range octetStreamTests { + octetStream := NewShortByteString(test.data) + + if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { + t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) + } + + expectedBitLength := uint16(len(test.data)) * 8 + if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { + t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) + } + + expectedEncodedLength := uint16(len(test.data)) + 1 + if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { + t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) + } + + encodedBytes := octetStream.EncodedBytes() + if !bytes.Equal(encodedBytes[1:], test.data) { + t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) + } + + encodedLength := int(encodedBytes[0]) + if encodedLength != len(test.data) { + t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) + } + + newStream := new(ShortByteString) + newStream.ReadFrom(bytes.NewReader(encodedBytes)) + + if !checkEquality(newStream, octetStream) { + t.Errorf("#%d: bad parsing of encoded octet stream", i) + } + } +} + +func checkEquality(left *ShortByteString, right *ShortByteString) bool { + return (left.length == right.length) && (bytes.Equal(left.data, right.data)) +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index a40e45be..73fb56e0 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -21,7 +21,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -308,6 +311,24 @@ 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) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -315,7 +336,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 { @@ -350,6 +372,29 @@ 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) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem_x25519 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/keys.go b/openpgp/keys.go index a071353e..34bffb97 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -134,6 +134,7 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 + isPQ := false var maxTime time.Time for i, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && @@ -142,9 +143,10 @@ func (e *Entity) EncryptionKey(now time.Time) (Key, bool) { !subkey.PublicKey.KeyExpired(subkey.Sig, now) && !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && - (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) { + (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime) || (!isPQ && subkey.IsPQ())) { candidateSubkey = i maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -201,6 +203,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 var maxTime time.Time + isPQ := false for idx, subkey := range e.Subkeys { if subkey.Sig.FlagsValid && (flags&packet.KeyFlagCertify == 0 || subkey.Sig.FlagCertify) && @@ -210,9 +213,11 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int) (Key, !subkey.Sig.SigExpired(now) && !subkey.Revoked(now) && (maxTime.IsZero() || subkey.Sig.CreationTime.After(maxTime)) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkey.Sig.CreationTime + isPQ = subkey.IsPQ() } } @@ -305,6 +310,11 @@ func (s *Subkey) Revoked(now time.Time) bool { return revoked(s.Revocations, now) } +// IsPQ returns true if the algorithm is Post-Quantum safe. +func (s *Subkey) IsPQ() bool { + return s.PublicKey.IsPQ() +} + // Revoked returns whether the key or subkey has been revoked by a self-signature. // Note that third-party revocation signatures are not supported. // Note also that Identity revocation should be checked separately. @@ -371,7 +381,7 @@ func (el EntityList) KeysByIdUsage(id uint64, requiredUsage byte) (keys []Key) { func (el EntityList) DecryptionKeys() (keys []Key) { for _, e := range el { for _, subKey := range e.Subkeys { - if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications) { + if subKey.PrivateKey != nil && subKey.Sig.FlagsValid && (subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications || subKey.Sig.FlagForward) { keys = append(keys, Key{e, subKey.PublicKey, subKey.PrivateKey, subKey.Sig, subKey.Revocations}) } } @@ -761,6 +771,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 +804,16 @@ func (e *Entity) Serialize(w io.Writer) error { } } for _, subkey := range e.Subkeys { + // The types of keys below are only useful as private keys. Thus, the + // public key packets contain no meaningful information and do not need + // to be serialized. + // Prevent public key export for forwarding keys, see forwarding section 4.1. + if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD || + subkey.Sig.FlagForward { + continue + } + err = subkey.PublicKey.Serialize(w) if err != nil { return err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 3cb4ac00..8bddbeb7 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -1169,6 +1170,191 @@ func TestAddSubkeySerialized(t *testing.T) { } } +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + func TestAddSubkeyWithConfig(t *testing.T) { c := &packet.Config{ DefaultHash: crypto.SHA512, @@ -1865,3 +2051,102 @@ 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") + } +} + +func testAddMlkemSubkey(t *testing.T, entity *Entity, v6Keys bool) { + var err error + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + for name, algo := range asymmAlgos { + // Remove existing subkeys + entity.Subkeys = []Subkey{} + + t.Run(name, func(t *testing.T) { + kyberConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: v6Keys, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + err = entity.AddEncryptionSubkey(kyberConfig) + if err != nil { + t.Fatal(err) + } + + if len(entity.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if entity.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + + if entity.Subkeys[0].PublicKey.Version != entity.PrivateKey.Version { + t.Fatalf("Expected subkey version: %d, got: %d", entity.PrivateKey.Version, + entity.Subkeys[0].PublicKey.Version) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(read.Subkeys) != 1 { + t.Fatalf("Expected 1 subkey, got %d", len(entity.Subkeys)) + } + + if read.Subkeys[0].PublicKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", packet.PubKeyAlgoEdDSA, + entity.Subkeys[0].PublicKey.PubKeyAlgo) + } + }) + } +} diff --git a/openpgp/keys_v6_test.go b/openpgp/keys_v6_test.go index fc9ba776..7914d3eb 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -3,8 +3,14 @@ package openpgp import ( "bytes" "crypto" + "crypto/rand" "strings" "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -196,3 +202,138 @@ func TestNewEntityWithDefaultHashv6(t *testing.T) { } } } + +func TestGeneratePqKey(t *testing.T) { + randomPassword := make([]byte, 128) + _, err := rand.Read(randomPassword) + if err != nil { + t.Fatal(err) + } + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for name, algo := range asymmAlgos { + t.Run(name, func(t *testing.T) { + config := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatalf("Failed to parse entity: %s", err) + } + + if read.PrimaryKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", algo, read.PrimaryKey.PubKeyAlgo) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err := read.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-DSA key was marked as invalid: ", err) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { + bin, err := pk.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + if pk.PublicMldsa, err = pk.Mldsa.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal(err) + } + } + + err = read.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-DSA key") + } + + testMlkemSubkey(t, read.Subkeys[0], randomPassword) + }) + } +} + +func testMlkemSubkey(t *testing.T, subkey Subkey, randomPassword []byte) { + var err error + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err = subkey.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-KEM key was marked as invalid: ", err) + } + + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + // Corrupt public ML-KEM in primary key + if pk, ok := subkey.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey); ok { + bin, _ := pk.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + if pk.PublicMlkem, err = pk.Mlkem.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal("unable to corrupt key") + } + } else { + t.Fatal("Invalid subkey") + } + + err = subkey.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-KEM key") + } +} + +func TestAddV6MlkemSubkey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + testAddMlkemSubkey(t, entity, true) +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go new file mode 100644 index 00000000..34c32d8b --- /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-ietf-openpgp-pqc-04.html#name-composite-signature-schemes +package mldsa_eddsa + +import ( + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/sign" +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.EdDSACurve + Mldsa sign.Scheme + PublicPoint []byte + PublicMldsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMldsa sign.PrivateKey +} + +// 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 sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mldsa = d + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateEdDSA(rand) + if err != nil { + return nil, err + } + + keySeed := make([]byte, d.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + + priv.PublicKey.PublicMldsa, priv.SecretMldsa = priv.PublicKey.Mldsa.DeriveKey(keySeed) + + return priv, nil +} + +// Sign generates a ML-DSA + EdDSA composite signature as specified in +// 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 { + return nil, nil, err + } + + dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message, nil) + if dSig == nil { + return nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") + } + + return dSig, ecSig, nil +} + +// Verify verifies a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-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, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateEdDSA(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicMldsa.Equal(priv.SecretMldsa.Public()) { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + + return nil +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go new file mode 100644 index 00000000..20b9e160 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -0,0 +1,95 @@ +// Package mldsa_eddsa_test tests the implementation of hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +package mldsa_eddsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA3_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicMldsa, err = key.Mldsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_eddsa.PrivateKey { + curveObj, err := packet.GetEdDSACurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMldsaFromAlgID(algId) + if err != nil { + t.Errorf("error getting ML-DSA: %s", err) + } + + priv, err := mldsa_eddsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *mldsa_eddsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, ecSig, err := mldsa_eddsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := mldsa_eddsa.Verify(&priv.PublicKey, digest, dSig, ecSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go new file mode 100644 index 00000000..c532d629 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -0,0 +1,266 @@ +// Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +// It follows the spec https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-04.html#name-composite-kem-schemes +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" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/kem" +) + +const ( + maxSessionKeyLength = 64 + domainSeparator = "OpenPGPCompositeKDFv1" +) + +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-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) + + 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, err := generateRandomSeed(rand, k.SeedSize()) + 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-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) > maxSessionKeyLength { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") + } + + if len(msg)%8 != 0 { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key not a multiple of 8") + } + + // EC shared secret derivation + ecEphemeral, ecSS, err := pub.Curve.Encaps(rand, pub.PublicPoint) + if err != nil { + return nil, nil, nil, err + } + + // ML-KEM shared secret derivation + kyberSeed, err := generateRandomSeed(rand, pub.Mlkem.EncapsulationSeedSize()) + if err != nil { + return nil, nil, nil, err + } + + kEphemeral, kSS, err := pub.Mlkem.EncapsulateDeterministically(pub.PublicMlkem, kyberSeed) + if err != nil { + return nil, nil, nil, err + } + + keyEncryptionKey, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS, kEphemeral, pub.PublicMlkem) + if err != nil { + return nil, nil, nil, err + } + + if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { + return nil, nil, nil, err + } + + return kEphemeral, ecEphemeral, ciphertext, nil +} + +// Decrypt implements ML-KEM + ECC decryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-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) + if err != nil { + return nil, err + } + + // ML-KEM shared secret derivation + kSS, err := priv.PublicKey.Mlkem.Decapsulate(priv.SecretMlkem, kEphemeral) + if err != nil { + return nil, err + } + + kek, err := buildKey(&priv.PublicKey, ecSS, ecEphemeral, priv.PublicPoint, kSS, kEphemeral, priv.PublicMlkem) + if err != nil { + return nil, err + } + + return keywrap.Unwrap(kek, ciphertext) +} + +// buildKey implements the composite KDF as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-combiner +func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { + h := sha3.New256() + + // SHA3 never returns error + _, _ = h.Write(eccSecretPoint) + _, _ = h.Write(eccEphemeral) + _, _ = h.Write(eccPublicKey) + eccKeyShare := h.Sum(nil) + + serializedMlkemPublicKey, err := mlkemPublicKey.MarshalBinary() + if err != nil { + return nil, err + } + + // 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 +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateECDH(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicKey.PublicMlkem.Equal(priv.SecretMlkem.Public()) { + return errors.KeyInvalidError("mlkem_ecdh: invalid public key") + } + + return +} + +// EncodeFields encodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(w io.Writer, ec, ml, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + if _, err = w.Write(ec); err != nil { + return err + } + + if _, err = w.Write(ml); err != nil { + return err + } + + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + + if _, err = w.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + + if !v6 { + if _, err = w.Write([]byte{cipherFunction}); err != nil { + return err + } + } + + if _, err = w.Write(encryptedSessionKey); err != nil { + return err + } + + return nil +} + +// DecodeFields decodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, encryptedMPI2, encryptedMPI3 encoding.Field, cipherFunction byte, err error) { + var buf [1]byte + + encryptedMPI1 = encoding.NewEmptyOctetArray(lenEcc) + if _, err = encryptedMPI1.ReadFrom(r); err != nil { + return + } + + encryptedMPI2 = encoding.NewEmptyOctetArray(lenMlkem) + if _, err = encryptedMPI2.ReadFrom(r); err != nil { + return + } + + // A one-octet size of the following fields. + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + cipherFunction = buf[0] + followingLen -= 1 + } + + // The encrypted session key. + encryptedMPI3 = encoding.NewEmptyOctetArray(int(followingLen)) + if _, err = encryptedMPI3.ReadFrom(r); err != nil { + return + } + + return +} + +func generateRandomSeed(rand io.Reader, size int) ([]byte, error) { + randomBytes := make([]byte, size) + if _, err := rand.Read(randomBytes); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + return randomBytes, nil +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go new file mode 100644 index 00000000..23a82eb3 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -0,0 +1,104 @@ +// Package mlkem_ecdh_test tests the implementation of hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +package mlkem_ecdh_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestEncryptDecrypt(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + symmAlgos := map[string]algorithm.Cipher{ + "AES-128": algorithm.AES128, + "AES-192": algorithm.AES192, + "AES-256": algorithm.AES256, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + for symmName, symmAlgo := range symmAlgos { + t.Run(symmName, func(t *testing.T) { + testEncryptDecryptAlgo(t, key, symmAlgo) + }) + } + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + var err error + key := testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, _ := key.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + key.PublicMlkem, err = key.Mlkem.UnmarshalBinaryPublicKey(bin) + if err != nil { + t.Fatal("unable to corrupt key") + } + + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_ecdh.PrivateKey { + curveObj, err := packet.GetECDHCurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMlkemFromAlgID(algId) + if err != nil { + t.Errorf("error getting kyber: %s", err) + } + + priv, err := mlkem_ecdh.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testEncryptDecryptAlgo(t *testing.T, priv *mlkem_ecdh.PrivateKey, kdfCipher algorithm.Cipher) { + expectedMessage := make([]byte, kdfCipher.KeySize()) // encryption algo + checksum + rand.Read(expectedMessage) + + kE, ecE, c, err := mlkem_ecdh.Encrypt(rand.Reader, &priv.PublicKey, expectedMessage) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + decryptedMessage, err := mlkem_ecdh.Decrypt(priv, kE, ecE, c) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + if !bytes.Equal(decryptedMessage, expectedMessage) { + t.Errorf("decryption failed, got: %x, want: %x", decryptedMessage, expectedMessage) + } +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 58340945..2da33cb1 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -17,7 +17,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdh" "github.com/ProtonMail/go-crypto/openpgp/elgamal" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -33,10 +36,15 @@ type EncryptedKey struct { CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt - encryptedMPI1, encryptedMPI2 encoding.Field - ephemeralPublicX25519 *x25519.PublicKey // used for x25519 - ephemeralPublicX448 *x448.PublicKey // used for x448 - encryptedSession []byte // used for x25519 and x448 + encryptedMPI1 encoding.Field // Only valid in RSA, Elgamal, ECDH, and PQC keys + encryptedMPI2 encoding.Field // Only valid in Elgamal, ECDH and PQC keys + encryptedMPI3 encoding.Field // Only valid in PQC keys + ephemeralPublicX25519 *x25519.PublicKey // used for x25519 + ephemeralPublicX448 *x448.PublicKey // used for x448 + encryptedSession []byte // used for x25519 and x448 + + nonce []byte + aeadMode algorithm.AEADMode } func (e *EncryptedKey) parse(r io.Reader) (err error) { @@ -133,12 +141,35 @@ 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 + } + case PubKeyAlgoMlkem768X25519: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 32, 1088, e.Version == 6); err != nil { + return err + } + case PubKeyAlgoMlkem1024X448: + if e.encryptedMPI1, e.encryptedMPI2, e.encryptedMPI3, cipherFunction, err = mlkem_ecdh.DecodeFields(r, 56, 1568, e.Version == 6); err != nil { + return err + } } if e.Version < 6 { switch e.Algo { - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: e.CipherFunc = CipherFunction(cipherFunction) - // Check for validiy is in the Decrypt method + // Check for validity is in the Decrypt method } } @@ -191,6 +222,15 @@ 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) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + ecE := e.encryptedMPI1.Bytes() + kE := e.encryptedMPI2.Bytes() + m := e.encryptedMPI3.Bytes() + + b, err = mlkem_ecdh.Decrypt(priv.PrivateKey.(*mlkem_ecdh.PrivateKey), kE, ecE, m) default: err = errors.InvalidArgumentError("cannot decrypt encrypted session key with private key of type " + strconv.Itoa(int(priv.PubKeyAlgo))) } @@ -200,7 +240,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]) @@ -210,22 +250,22 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } } key, err = decodeChecksumKey(b[keyOffset:]) - if err != nil { - return err - } - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: if e.Version < 6 { switch e.CipherFunc { case CipherAES128, CipherAES192, CipherAES256: break default: - return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519 and x448") + return errors.StructuralError("v3 PKESK mandates AES as cipher function for x25519, x448, and PQC") } } key = b[:] default: return errors.UnsupportedError("unsupported algorithm for decryption") } + if err != nil { + return err + } e.Key = key return nil } @@ -244,6 +284,11 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { encodedLength = x25519.EncodedFieldsLength(e.encryptedSession, e.Version == 6) case PubKeyAlgoX448: encodedLength = x448.EncodedFieldsLength(e.encryptedSession, e.Version == 6) + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + encodedLength = int(e.encryptedMPI1.EncodedLength()) + int(e.encryptedMPI2.EncodedLength()) + int(e.encryptedMPI3.EncodedLength()) + 1 + if e.Version < 6 { + encodedLength += 1 + } default: return errors.InvalidArgumentError("don't know how to serialize encrypted key type " + strconv.Itoa(int(e.Algo))) } @@ -314,6 +359,9 @@ func (e *EncryptedKey) Serialize(w io.Writer) error { case PubKeyAlgoX448: err := x448.EncodeFields(w, e.ephemeralPublicX448, e.encryptedSession, byte(e.CipherFunc), e.Version == 6) return err + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + err := mlkem_ecdh.EncodeFields(w, e.encryptedMPI1.EncodedBytes(), e.encryptedMPI2.EncodedBytes(), e.encryptedMPI3.EncodedBytes(), byte(e.CipherFunc), e.Version == 6) + return err default: panic("internal error") } @@ -344,13 +392,13 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal { return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed") } - // In v3 PKESKs, for x25519 and x448, mandate using AES - if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) { - switch cipherFunc { - case CipherAES128, CipherAES192, CipherAES256: - break + // In v3 PKESKs, for X25519 and X448, mandate using AES + if version == 3 && cipherFunc != CipherAES128 && cipherFunc != CipherAES192 && cipherFunc != CipherAES256 { + switch pub.PubKeyAlgo { + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519, x448, and PQC") default: - return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448") + break } } @@ -387,7 +435,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 @@ -399,7 +447,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: // algorithm is added in plaintext below keyBlock = key } @@ -415,7 +463,11 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph return serializeEncryptedKeyX25519(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x25519.PublicKey), keyBlock, byte(cipherFunc), version) case PubKeyAlgoX448: return serializeEncryptedKeyX448(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*x448.PublicKey), keyBlock, byte(cipherFunc), version) - case PubKeyAlgoDSA, PubKeyAlgoRSASignOnly: + case ExperimentalPubKeyAlgoAEAD: + return serializeEncryptedKeyAEAD(w, config.Random(), buf[:lenHeaderWritten], pub.PublicKey.(*symmetric.AEADPublicKey), keyBlock, config.AEAD()) + case 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))) } @@ -438,6 +490,36 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } +func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { + if e.Algo != PubKeyAlgoECDH { + return nil, errors.InvalidArgumentError("invalid PKESK") + } + + if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { + return nil, errors.InvalidArgumentError("invalid key id in PKESK") + } + + ephemeral := e.encryptedMPI1.Bytes() + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, instance.ProxyParameter) + if err != nil { + return nil, err + } + + wrappedKey := e.encryptedMPI2.Bytes() + copiedWrappedKey := make([]byte, len(wrappedKey)) + copy(copiedWrappedKey, wrappedKey) + + transformed = &EncryptedKey{ + Version: e.Version, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), + } + + return transformed, nil +} + func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) if err != nil { @@ -554,6 +636,35 @@ func serializeEncryptedKeyX448(w io.Writer, rand io.Reader, header []byte, pub * return x448.EncodeFields(w, ephemeralPublicX448, ciphertext, cipherFunc, version == 6) } +func serializeEncryptedKeyAEAD(w io.Writer, rand io.Reader, header []byte, pub *symmetric.AEADPublicKey, keyBlock []byte, config *AEADConfig) error { + mode := algorithm.AEADMode(config.Mode()) + iv, ciphertextRaw, err := pub.Encrypt(rand, keyBlock, mode) + if err != nil { + return errors.InvalidArgumentError("AEAD encryption failed: " + err.Error()) + } + + ciphertextShortByteString := encoding.NewShortByteString(ciphertextRaw) + + buffer := append([]byte{byte(mode)}, iv...) + buffer = append(buffer, ciphertextShortByteString.EncodedBytes()...) + + packetLen := len(header) /* header length */ + packetLen += int(len(buffer)) + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header[:]) + if err != nil { + return err + } + + _, err = w.Write(buffer) + return err +} + func checksumKeyMaterial(key []byte) uint16 { var checksum uint16 for _, v := range key { @@ -578,3 +689,32 @@ func encodeChecksumKey(buffer []byte, key []byte) { buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } + +func serializeEncryptedKeyMlkem(w io.Writer, rand io.Reader, header []byte, pub *mlkem_ecdh.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + mlE, ecE, c, err := mlkem_ecdh.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("ML-KEM + ECDH encryption failed: " + err.Error()) + } + + ml := encoding.NewOctetArray(mlE) + ec := encoding.NewOctetArray(ecE) + m := encoding.NewOctetArray(c) + + packetLen := len(header) /* header length */ + packetLen += int(ec.EncodedLength()) + int(ml.EncodedLength()) + int(m.EncodedLength()) + 1 + if version < 6 { + packetLen += 1 + } + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header) + if err != nil { + return err + } + + return mlkem_ecdh.EncodeFields(w, ec.EncodedBytes(), ml.EncodedBytes(), m.EncodedBytes(), cipherFunc, version == 6) +} diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index 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/forwarding.go b/openpgp/packet/forwarding.go new file mode 100644 index 00000000..f16a2fbd --- /dev/null +++ b/openpgp/packet/forwarding.go @@ -0,0 +1,36 @@ +package packet + +import "encoding/binary" + +// ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) +type ForwardingInstance struct { + KeyVersion int + ForwarderFingerprint []byte + ForwardeeFingerprint []byte + ProxyParameter []byte +} + +func (f *ForwardingInstance) GetForwarderKeyId() uint64 { + return computeForwardingKeyId(f.ForwarderFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) GetForwardeeKeyId() uint64 { + return computeForwardingKeyId(f.ForwardeeFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) getForwardeeKeyIdOrZero(originalKeyId uint64) uint64 { + if originalKeyId == 0 { + return 0 + } + + return f.GetForwardeeKeyId() +} + +func computeForwardingKeyId(fingerprint []byte, version int) uint64 { + switch version { + case 4: + return binary.BigEndian.Uint64(fingerprint[12:20]) + default: + panic("invalid pgp key version") + } +} diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 1e92e22c..43e79ee2 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -506,16 +506,28 @@ 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 + + // Experimental PQC KEM algorithms + PubKeyAlgoMlkem768X25519 = 105 + PubKeyAlgoMlkem1024X448 = 106 + + // Experimental PQC DSA algorithms + PubKeyAlgoMldsa65Ed25519 = 107 + PubKeyAlgoMldsa87Ed448 = 108 ) // CanEncrypt returns true if it's possible to encrypt a message to a public // key of the given type. func (pka PublicKeyAlgorithm) CanEncrypt() bool { switch pka { - case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, PubKeyAlgoX25519, PubKeyAlgoX448, ExperimentalPubKeyAlgoAEAD, + PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: return true } return false @@ -525,7 +537,8 @@ 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, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: return true } return false diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index f04e6c6b..b0cc8b8f 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -13,6 +13,7 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" + goerrors "errors" "fmt" "io" "math/big" @@ -27,9 +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" ) @@ -166,6 +174,10 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) case ed448.PrivateKey: pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) + case *symmetric.HMACPrivateKey: + pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) + case *mldsa_eddsa.PrivateKey: + pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -173,7 +185,7 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey return pk } -// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448}.PrivateKey. +// NewDecrypterPrivateKey creates a PrivateKey from a *{rsa|elgamal|ecdh|x25519|x448|mlkem_ecdh}.PrivateKey. func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *PrivateKey { pk := new(PrivateKey) switch priv := decrypter.(type) { @@ -187,6 +199,10 @@ func NewDecrypterPrivateKey(creationTime time.Time, decrypter interface{}) *Priv pk.PublicKey = *NewX25519PublicKey(creationTime, &priv.PublicKey) case *x448.PrivateKey: pk.PublicKey = *NewX448PublicKey(creationTime, &priv.PublicKey) + case *symmetric.AEADPrivateKey: + pk.PublicKey = *NewAEADPublicKey(creationTime, &priv.PublicKey) + case *mlkem_ecdh.PrivateKey: + pk.PublicKey = *NewMlkemEcdhPublicKey(creationTime, &priv.PublicKey) default: panic("openpgp: unknown decrypter type in NewDecrypterPrivateKey") } @@ -530,6 +546,54 @@ 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 err +} + +// serializeMlkemPrivateKey serializes a ML-KEM + ECC private key according to +// 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 { + return err + } + if _, err = w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + _, err = w.Write(encoding.NewOctetArray(kyberBin).EncodedBytes()) + return err +} + +// serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to +// 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 + } + 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. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -830,6 +894,14 @@ func (pk *PrivateKey) serializePrivateKey(w io.Writer) (err error) { err = serializeEd25519PrivateKey(w, priv) case *ed448.PrivateKey: err = serializeEd448PrivateKey(w, priv) + case *symmetric.AEADPrivateKey: + err = serializeAEADPrivateKey(w, priv) + case *symmetric.HMACPrivateKey: + err = serializeHMACPrivateKey(w, priv) + case *mlkem_ecdh.PrivateKey: + err = serializeMlkemPrivateKey(w, priv) + case *mldsa_eddsa.PrivateKey: + err = serializeMldsaEddsaPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -858,6 +930,18 @@ func (pk *PrivateKey) parsePrivateKey(data []byte) (err error) { return pk.parseEd25519PrivateKey(data) case PubKeyAlgoEd448: return pk.parseEd448PrivateKey(data) + case ExperimentalPubKeyAlgoAEAD: + return pk.parseAEADPrivateKey(data) + case ExperimentalPubKeyAlgoHMAC: + return pk.parseHMACPrivateKey(data) + case PubKeyAlgoMlkem768X25519: + return pk.parseMlkemEcdhPrivateKey(data, 32, mlkem768.PrivateKeySize) + case PubKeyAlgoMlkem1024X448: + return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem1024.PrivateKeySize) + case PubKeyAlgoMldsa65Ed25519: + return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa65.PrivateKeySize) + case PubKeyAlgoMldsa87Ed448: + return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa87.PrivateKeySize) default: err = errors.StructuralError("unknown private key type") return @@ -1121,6 +1205,131 @@ 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 +} + +// parseMldsaEddsaPrivateKey parses a ML-DSA + EdDSA private key as specified in +// 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") + } + 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, err = priv.Mldsa.UnmarshalBinaryPrivateKey(d.Bytes()) + if err != nil { + return err + } + if err := mldsa_eddsa.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseMlkemEcdhPrivateKey parses a ML-KEM + ECC private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-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) + 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 +} + 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 f279789d..f93723cd 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,12 +5,14 @@ package packet import ( + "bytes" "crypto/dsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" _ "crypto/sha512" "encoding/binary" + goerrors "errors" "fmt" "hash" "io" @@ -28,8 +30,17 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" + "github.com/cloudflare/circl/kem" + "github.com/cloudflare/circl/kem/mlkem/mlkem1024" + "github.com/cloudflare/circl/kem/mlkem/mlkem768" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. @@ -37,7 +48,7 @@ type PublicKey struct { Version int CreationTime time.Time PubKeyAlgo PublicKeyAlgorithm - PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey + PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey, or *mlkem_ecdh.PublicKey Fingerprint []byte KeyId uint64 IsSubkey bool @@ -68,6 +79,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 { @@ -229,6 +260,65 @@ func NewEd448PublicKey(creationTime time.Time, pub *ed448.PublicKey) *PublicKey return pk } +func NewAEADPublicKey(creationTime time.Time, pub *symmetric.AEADPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoAEAD, + PublicKey: pub, + } + + return pk +} + +func NewHMACPublicKey(creationTime time.Time, pub *symmetric.HMACPublicKey) *PublicKey { + pk := &PublicKey{ + Version: 4, + CreationTime: creationTime, + PubKeyAlgo: ExperimentalPubKeyAlgoHMAC, + PublicKey: pub, + } + + return pk +} + +func NewMlkemEcdhPublicKey(creationTime time.Time, pub *mlkem_ecdh.PublicKey) *PublicKey { + mlkemBin, err := pub.PublicMlkem.MarshalBinary() + if err != nil { + panic(err) + } + + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(mlkemBin), + } + + pk.setFingerprintAndKeyId() + return pk +} + +func NewMldsaEddsaPublicKey(creationTime time.Time, pub *mldsa_eddsa.PublicKey) *PublicKey { + publicKeyBytes, err := pub.PublicMldsa.MarshalBinary() + if err != nil { + panic(err) + } + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + p: encoding.NewOctetArray(pub.PublicPoint), + q: encoding.NewOctetArray(publicKeyBytes), + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -257,7 +347,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) @@ -279,6 +369,18 @@ 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) + case PubKeyAlgoMlkem768X25519: + err = pk.parseMlkemEcdh(r, 32, mlkem768.PublicKeySize) + case PubKeyAlgoMlkem1024X448: + err = pk.parseMlkemEcdh(r, 56, mlkem1024.PublicKeySize) + case PubKeyAlgoMldsa65Ed25519: + err = pk.parseMldsaEddsa(r, 32, mldsa65.PublicKeySize) + case PubKeyAlgoMldsa87Ed448: + err = pk.parseMldsaEddsa(r, 57, mldsa87.PublicKeySize) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -451,11 +553,13 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid)) } - if kdfLen := len(pk.kdf.Bytes()); kdfLen < 3 { + kdfLen := len(pk.kdf.Bytes()) + if kdfLen < 3 { return errors.UnsupportedError("unsupported ECDH KDF length: " + strconv.Itoa(kdfLen)) } - if reserved := pk.kdf.Bytes()[0]; reserved != 0x01 { - return errors.UnsupportedError("unsupported KDF reserved field: " + strconv.Itoa(int(reserved))) + kdfVersion := int(pk.kdf.Bytes()[0]) + if kdfVersion != ecdh.KDFVersion1 && kdfVersion != ecdh.KDFVersionForwarding { + return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(kdfVersion)) } kdfHash, ok := algorithm.HashById[pk.kdf.Bytes()[1]] if !ok { @@ -466,9 +570,57 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError("unsupported ECDH KDF cipher: " + strconv.Itoa(int(pk.kdf.Bytes()[2]))) } - ecdhKey := ecdh.NewPublicKey(c, kdfHash, kdfCipher) + kdf := ecdh.KDF{ + Version: kdfVersion, + Hash: kdfHash, + Cipher: kdfCipher, + } + + if kdfVersion == ecdh.KDFVersionForwarding { + if pk.Version != 4 || kdfLen != 23 { + return errors.UnsupportedError("unsupported ECDH KDF v2 length: " + strconv.Itoa(kdfLen)) + } + + kdf.ReplacementFingerprint = pk.kdf.Bytes()[3:23] + } + + ecdhKey := ecdh.NewPublicKey(c, kdf) err = ecdhKey.UnmarshalPoint(pk.p.Bytes()) pk.PublicKey = ecdhKey + return +} + +// parseMlkemEcdh parses a ML-KEM + ECC public key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-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 { + 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 } @@ -566,6 +718,92 @@ func (pk *PublicKey) parseEd448(r io.Reader) (err error) { return } +func (pk *PublicKey) parseAEAD(r io.Reader) (err error) { + var cipher [1]byte + _, err = readFull(r, cipher[:]) + if err != nil { + return + } + + var bindingHash [32]byte + _, err = readFull(r, bindingHash[:]) + if err != nil { + return + } + + symmetric := &symmetric.AEADPublicKey{ + Cipher: algorithm.CipherFunction(cipher[0]), + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func (pk *PublicKey) parseHMAC(r io.Reader) (err error) { + var hash [1]byte + _, err = readFull(r, hash[:]) + if err != nil { + return + } + bindingHash, err := readBindingHash(r) + if err != nil { + return + } + + hmacHash, ok := algorithm.HashById[hash[0]] + if !ok { + return errors.UnsupportedError("unsupported HMAC hash: " + strconv.Itoa(int(hash[0]))) + } + + symmetric := &symmetric.HMACPublicKey{ + Hash: hmacHash, + BindingHash: bindingHash, + } + + pk.PublicKey = symmetric + return +} + +func readBindingHash(r io.Reader) (bindingHash [32]byte, err error) { + _, err = readFull(r, bindingHash[:]) + return bindingHash, err +} + +// parseMldsaEddsa parses a ML-DSA + EdDSA public key as specified in +// 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 { + return + } + + pk.q = encoding.NewEmptyOctetArray(dLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mldsa_eddsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetEdDSACurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMldsa, err = pub.Mldsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub + return +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -653,6 +891,13 @@ func (pk *PublicKey) algorithmSpecificByteCount() uint32 { length += ed25519.PublicKeySize case PubKeyAlgoEd448: length += ed448.PublicKeySize + case ExperimentalPubKeyAlgoAEAD, ExperimentalPubKeyAlgoHMAC: + length += 1 // Hash octet + length += 32 // Binding hash + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + length += uint32(pk.p.EncodedLength()) + length += uint32(pk.q.EncodedLength()) default: panic("unknown public key algorithm") } @@ -745,13 +990,36 @@ func (pk *PublicKey) serializeWithoutHeaders(w io.Writer) (err error) { publicKey := pk.PublicKey.(*ed448.PublicKey) _, err = w.Write(publicKey.Point) return + case ExperimentalPubKeyAlgoAEAD: + symmKey := pk.PublicKey.(*symmetric.AEADPublicKey) + cipherOctet := [1]byte{symmKey.Cipher.Id()} + if _, err = w.Write(cipherOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return + case ExperimentalPubKeyAlgoHMAC: + symmKey := pk.PublicKey.(*symmetric.HMACPublicKey) + hashOctet := [1]byte{symmKey.Hash.Id()} + if _, err = w.Write(hashOctet[:]); err != nil { + return + } + _, err = w.Write(symmKey.BindingHash[:]) + return + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(pk.p.EncodedBytes()); err != nil { + return + } + _, err = w.Write(pk.q.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } // CanSign returns true iff this public key can generate signatures func (pk *PublicKey) CanSign() bool { - return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH + return pk.PubKeyAlgo.CanSign() } // VerifyHashTag returns nil iff sig appears to be a plausible signature of the data @@ -831,6 +1099,23 @@ func (pk *PublicKey) VerifySignature(signed hash.Hash, sig *Signature) (err erro return errors.SignatureError("ed448 verification failure") } return nil + case ExperimentalPubKeyAlgoHMAC: + HMACKey := pk.PublicKey.(*symmetric.HMACPublicKey) + + result, err := HMACKey.Verify(hashBytes, sig.HMAC.Bytes()) + if err != nil { + return err + } + if !result { + return errors.SignatureError("HMAC verification failure") + } + return nil + 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 default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -901,6 +1186,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 } @@ -1052,6 +1344,11 @@ func (pk *PublicKey) BitLength() (bitLength uint16, err error) { bitLength = ed25519.PublicKeySize * 8 case PubKeyAlgoEd448: bitLength = ed448.PublicKeySize * 8 + case ExperimentalPubKeyAlgoAEAD: + bitLength = 32 + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, PubKeyAlgoMldsa65Ed25519, + PubKeyAlgoMldsa87Ed448: + bitLength = pk.q.BitLength() // TODO: Discuss if this makes sense. default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1090,3 +1387,71 @@ func (pk *PublicKey) KeyExpired(sig *Signature, currentTime time.Time) bool { expiry := pk.CreationTime.Add(time.Duration(*sig.KeyLifetimeSecs) * time.Second) return currentTime.Unix() > expiry.Unix() } + +// IsPQ returns true if the algorithm of this public key is Post-Quantum safe. +func (pg *PublicKey) IsPQ() bool { + switch pg.PubKeyAlgo { + case PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448, + PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } +} + +func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return PubKeyAlgoMlkem768X25519, nil + case PubKeyAlgoMldsa87Ed448: + return PubKeyAlgoMlkem1024X448, nil + default: + return 0, goerrors.New("packet: unsupported pq public key algorithm") + } +} + +// GetMlkemFromAlgID returns the ML-KEM instance from the matching KEM +func GetMlkemFromAlgID(algId PublicKeyAlgorithm) (kem.Scheme, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return mlkem768.Scheme(), nil + case PubKeyAlgoMlkem1024X448: + return mlkem1024.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-KEM public key algorithm") + } +} + +// GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM +func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { + switch algId { + case PubKeyAlgoMlkem768X25519: + return ecc.NewCurve25519(), nil + case PubKeyAlgoMlkem1024X448: + return ecc.NewX448(), nil + default: + return nil, goerrors.New("packet: unsupported ECDH public key algorithm") + } +} + +func GetEdDSACurveFromAlgID(algId PublicKeyAlgorithm) (ecc.EdDSACurve, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return ecc.NewEd25519(), nil + case PubKeyAlgoMldsa87Ed448: + return ecc.NewEd448(), nil + default: + return nil, goerrors.New("packet: unsupported EdDSA public key algorithm") + } +} + +func GetMldsaFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519: + return mldsa65.Scheme(), nil + case PubKeyAlgoMldsa87Ed448: + return mldsa87.Scheme(), nil + default: + return nil, goerrors.New("packet: unsupported ML-DSA public key algorithm") + } +} diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 90097951..cacbb80c 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -21,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 ( @@ -31,7 +34,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -64,7 +67,10 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field + HMAC encoding.Field EdSig []byte + MldsaSig encoding.Field + SlhdsaSig encoding.Field // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -101,8 +107,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. @@ -172,7 +179,8 @@ func (sig *Signature) parse(r io.Reader) (err error) { sig.SigType = SignatureType(buf[0]) sig.PubKeyAlgo = PublicKeyAlgorithm(buf[1]) switch sig.PubKeyAlgo { - case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, PubKeyAlgoEd448: + case PubKeyAlgoRSA, PubKeyAlgoRSASignOnly, PubKeyAlgoDSA, PubKeyAlgoECDSA, PubKeyAlgoEdDSA, PubKeyAlgoEd25519, + PubKeyAlgoEd448, ExperimentalPubKeyAlgoHMAC, PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -310,12 +318,38 @@ 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 + } + case PubKeyAlgoMldsa65Ed25519: + if err = sig.parseMldsaEddsaSignature(r, 64, mldsa65.SignatureSize); err != nil { + return + } + case PubKeyAlgoMldsa87Ed448: + if err = sig.parseMldsaEddsaSignature(r, 114, mldsa87.SignatureSize); err != nil { + return + } default: panic("unreachable") } return } +// parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-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 { + 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) { @@ -542,6 +576,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 } @@ -953,6 +990,22 @@ 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) + } + 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) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1058,7 +1111,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.SlhdsaSig == nil && sig.HMAC == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -1079,6 +1132,11 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { sigLength = ed25519.SignatureSize case PubKeyAlgoEd448: sigLength = ed448.SignatureSize + case ExperimentalPubKeyAlgoHMAC: + sigLength = int(sig.HMAC.EncodedLength()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + sigLength = int(sig.EdDSASigR.EncodedLength()) + sigLength += int(sig.MldsaSig.EncodedLength()) default: panic("impossible") } @@ -1185,6 +1243,13 @@ func (sig *Signature) serializeBody(w io.Writer) (err error) { err = ed25519.WriteSignature(w, sig.EdSig) case PubKeyAlgoEd448: err = ed448.WriteSignature(w, sig.EdSig) + case ExperimentalPubKeyAlgoHMAC: + _, err = w.Write(sig.HMAC.EncodedBytes()) + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoMldsa87Ed448: + if _, err = w.Write(sig.EdDSASigR.EncodedBytes()); err != nil { + return + } + _, err = w.Write(sig.MldsaSig.EncodedBytes()) default: panic("impossible") } @@ -1284,6 +1349,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp if sig.FlagAuthenticate { flags |= KeyFlagAuthenticate } + if sig.FlagForward { + flags |= KeyFlagForward + } if sig.FlagGroupKey { flags |= KeyFlagGroupKey } diff --git a/openpgp/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..1a5f18d0 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -23,6 +23,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -118,7 +121,9 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 318d927e..50d9c3c4 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -12,6 +12,7 @@ import ( "io" "io/ioutil" "os" + "strconv" "strings" "testing" @@ -28,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 { @@ -750,7 +758,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) @@ -932,3 +940,99 @@ 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 670d6022..6b38f542 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -455,3 +455,395 @@ 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----- +` + +// 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/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 new file mode 100644 index 00000000..b9d389dc --- /dev/null +++ b/openpgp/symmetric/aead.go @@ -0,0 +1,75 @@ +package symmetric + +import ( + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "io" +) + +type AEADPublicKey struct { + Cipher algorithm.CipherFunction + BindingHash [32]byte + Key []byte +} + +type AEADPrivateKey struct { + PublicKey AEADPublicKey + HashSeed [32]byte + Key []byte +} + +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv, err = generatePrivatePartAEAD(rand, cipher) + if err != nil { + return + } + + priv.generatePublicPartAEAD(cipher) + return +} + +func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv = new(AEADPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, cipher.KeySize()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *AEADPrivateKey) generatePublicPartAEAD(cipher algorithm.CipherFunction) (err error) { + priv.PublicKey.Cipher = cipher + + bindingHash := ComputeBindingHash(priv.HashSeed) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + copy(priv.PublicKey.BindingHash[:], bindingHash) + return +} + +func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AEADMode) (nonce []byte, ciphertext []byte, err error) { + block := pub.Cipher.New(pub.Key) + aead := mode.New(block) + nonce = make([]byte, aead.NonceSize()) + rand.Read(nonce) + ciphertext = aead.Seal(nil, nonce, data, nil) + return +} + +func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + + block := priv.PublicKey.Cipher.New(priv.Key) + aead := mode.New(block) + message, err = aead.Open(nil, nonce, ciphertext, nil) + return +} diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go new file mode 100644 index 00000000..e9d61475 --- /dev/null +++ b/openpgp/symmetric/hmac.go @@ -0,0 +1,109 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "crypto/sha256" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +type HMACPublicKey struct { + Hash algorithm.Hash + BindingHash [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for verification. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type HMACPrivateKey struct { + PublicKey HMACPublicKey + HashSeed [32]byte + Key []byte +} + +func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv, err = generatePrivatePartHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartHMAC(hash) + return +} + +func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv = new(HMACPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, hash.Size()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { + priv.PublicKey.Hash = hash + + bindingHash := ComputeBindingHash(priv.HashSeed) + copy(priv.PublicKey.BindingHash[:], bindingHash) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + return +} + +func ComputeBindingHash(seed [32]byte) []byte { + bindingHash := sha256.New() + bindingHash.Write(seed[:]) + + return bindingHash.Sum(nil) +} + +func (priv *HMACPrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +func (priv *HMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + expectedMAC, err := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + if err != nil { + return + } + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *HMACPublicKey) Verify(digest []byte, signature []byte) (bool, error) { + expectedMAC, err := calculateMAC(pub.Hash, pub.Key, digest) + if err != nil { + return false, err + } + return hmac.Equal(expectedMAC, signature), nil +} + +func calculateMAC(hash algorithm.Hash, key []byte, data []byte) ([]byte, error) { + hashFunc := hash.HashFunc() + if !hashFunc.Available() { + return nil, errors.UnsupportedError("hash function") + } + + mac := hmac.New(hashFunc.New, key) + mac.Write(data) + + return mac.Sum(nil), nil +} diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go new file mode 100644 index 00000000..1306c510 --- /dev/null +++ b/openpgp/v2/forwarding.go @@ -0,0 +1,159 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + + if _, err = e.VerifyPrimaryKey(now, config); err != nil { + return nil, nil, err + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + keyProperties := selectKeyProperties(now, config, primary) + err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, keyProperties) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now, config) + // Filter expiration & revokal + if err != nil { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + forwardeeSubKeySelfSig := forwardeeSubKey.Bindings[0].Packet + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKeySelfSig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKeySelfSig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKeySelfSig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKeySelfSig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKeySelfSig.FlagForward = true + + err = forwardeeSubKeySelfSig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/v2/forwarding_test.go b/openpgp/v2/forwarding_test.go new file mode 100644 index 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] +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index 5537d4f8..167a4068 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,7 +21,10 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -387,6 +390,24 @@ 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) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mldsa_eddsa key") + } + + c, err := packet.GetEdDSACurveFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + d, err := packet.GetMldsaFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return mldsa_eddsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), c, d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -394,6 +415,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() @@ -429,6 +451,29 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { return x25519.GenerateKey(config.Random()) case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey return x448.GenerateKey(config.Random()) + case packet.ExperimentalPubKeyAlgoHMAC, packet.ExperimentalPubKeyAlgoAEAD: // When passing HMAC, we generate an AEAD subkey + cipher := algorithm.CipherFunction(config.Cipher()) + return symmetric.AEADGenerateKey(config.Random(), cipher) + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + if pubKeyAlgo, err = packet.GetMatchingMlkem(config.PublicKeyAlgorithm()); err != nil { + return nil, err + } + fallthrough // When passing ML-DSA + EdDSA or ECDSA, we generate a ML-KEM + ECDH subkey + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 mlkem_x25519 key") + } + + c, err := packet.GetECDHCurveFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + k, err := packet.GetMlkemFromAlgID(pubKeyAlgo) + if err != nil { + return nil, err + } + + return mlkem_ecdh.GenerateKey(config.Random(), uint8(pubKeyAlgo), c, k) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index b4a7cc1e..892c5fc1 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) } @@ -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 } } @@ -163,12 +165,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 @@ -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() } } @@ -608,6 +613,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 +637,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{}, nil) + 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 } @@ -786,3 +805,9 @@ func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgo signature.FlagsValid && (signature.FlagEncryptCommunications || signature.FlagEncryptStorage) } + +func isValidDecryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanEncrypt() && + signature.FlagsValid && + (signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage) +} 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..417bda09 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -26,6 +26,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -136,9 +139,9 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, - packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448: break default: continue @@ -741,12 +744,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 2feaf392..8b94d023 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" @@ -28,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 { @@ -1002,3 +1010,100 @@ func testMalformedMessage(t *testing.T, keyring EntityList, message string) { return } } + +var pqcDraftVectors = map[string]struct { + armoredPrivateKey string + armoredPublicKey string + fingerprints []string + armoredMessages []string + v6 bool +}{ + // TODO: Update with fresh test vectors + /*"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 2f0efc22..51d2027c 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -740,3 +740,395 @@ 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----- +` + +// 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/v2/subkeys.go b/openpgp/v2/subkeys.go index 9dc70899..db7e39cb 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -208,3 +208,15 @@ func (s *Subkey) LatestValidBindingSignature(date time.Time, config *packet.Conf } return } + +// IsPQ returns true if the algorithm is Post-Quantum safe +func (s *Subkey) IsPQ() bool { + switch s.PublicKey.PubKeyAlgo { + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } + +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 5146607c..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,8 +649,12 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index f3c4f9da..ef136665 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -6,6 +6,7 @@ package v2 import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -434,33 +435,6 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { - keyRingHex string - isSigned bool - okV6 bool -}{ - { - testKeys1And2PrivateHex, - false, - true, - }, - { - testKeys1And2PrivateHex, - true, - true, - }, - { - dsaElGamalTestKeysHex, - false, - false, - }, - { - dsaElGamalTestKeysHex, - true, - false, - }, -} - func TestIntendedRecipientsEncryption(t *testing.T) { var config = &packet.Config{ V6Keys: true, @@ -674,129 +648,185 @@ 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, + },*/ +} - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) +func TestEncryption(t *testing.T) { + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - config := allowAllAlgorithmsConfig - config.DefaultCompressionAlgo = compAlgo - config.CompressionConfig = compConf - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed []*Entity + if test.isSigned { + signed = kring[:1] } - config.AEADConfig = &aeadConf - } - var signers []*Entity - if signed != nil { - signers = []*Entity{signed} - } - w, err := Encrypt(buf, kring[:1], nil, signers, nil /* no hints */, &config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = compAlgo + config.CompressionConfig = compConf + config.DefaultCipher = packet.CipherAES256 + + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], nil, signed, nil /* no hints */, &config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + return + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := signKey.PublicKey.KeyId - if len(md.SignatureCandidates) < 1 { - t.Error("no candidate signature found") + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) } - if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignatureCandidates[0].SignedByEntity == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") - encryptKey, _ := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("no candidate signature found") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%s: message signed by wrong key id, got: %v, want: %v", name, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%s: failed to find the signing Entity", name) + } + } - if test.isSigned { - if !md.IsVerified { - t.Errorf("not verified despite all data read") + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) } - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + + encryptKey, out := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + if !out { + t.Fatalf("#%s: No encryption key found", name) } - if md.Signature == nil { - t.Error("signature missing") + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - } + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) + } + + if test.isSigned { + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -997,3 +1027,90 @@ FindKey: } return nil } + +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, []*Entity{entity}, msg, c) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, _, err = VerifyDetachedSignature(entityList, msg, sig, c) + if err != nil { + t.Fatal(err) + } +} diff --git a/openpgp/write.go b/openpgp/write.go index 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]) diff --git a/openpgp/write_test.go b/openpgp/write_test.go index c928236b..b6e3d7ed 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -6,6 +6,7 @@ package openpgp import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -263,6 +264,91 @@ func TestNewEntity(t *testing.T) { } } +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, entity, msg, nil) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, err = CheckDetachedSignature(entityList, msg, sig, nil) + if err != nil { + t.Fatal(err) + } +} + func TestEncryptWithCompression(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) passphrase := []byte("passphrase") @@ -436,148 +522,171 @@ 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, + },*/ } func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) - - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - var signed *Entity - if test.isSigned { - signed = kring[0] - } - - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - var config = &packet.Config{ - DefaultCompressionAlgo: compAlgo, - CompressionConfig: compConf, - } - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed *Entity + if test.isSigned { + signed = kring[0] } - config.AEADConfig = &aeadConf - } - w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + var config = &packet.Config{ + DefaultCompressionAlgo: compAlgo, + CompressionConfig: compConf, + DefaultCipher: packet.CipherAES256, + } - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime) - expectedKeyId := signKey.PublicKey.KeyId - if md.SignedByKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId) + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignedBy == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - encryptKey, _ := kring[0].EncryptionKey(testTime) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime) + expectedKeyId := signKey.PublicKey.KeyId + if md.SignedByKeyId != expectedKeyId { + t.Errorf("Message signed by wrong key id, got: %v, want: %v", *md.SignedBy, expectedKeyId) + } + if md.SignedBy == nil { + t.Error("#Failed to find the signing Entity") + } + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } - if test.isSigned { - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + encryptKey, _ := kring[0].EncryptionKey(testTime) + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - if md.Signature == nil { - t.Error("signature missing") + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) } - } + + if test.isSigned { + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -698,7 +807,8 @@ ParsePackets: case *packet.EncryptedKey: // This packet contains the decryption key encrypted to a public key. switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue