diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1a05c9196..6874dcae8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [ main ] + branches: [ main, Proton ] pull_request: - branches: [ main ] + branches: [ main, Proton ] jobs: @@ -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 d417da35c..e59aeab28 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ 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 + +replace github.com/cloudflare/circl v1.5.0 => github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93 diff --git a/go.sum b/go.sum index 712b2d44b..dad91470d 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/lubux/circl v0.0.0-20241113220611-a91ad6141f93 h1:lLX4wx3iE1uDt6v7pjcl2P8z4xTFHsp/1wOTRO+NPfg= +github.com/lubux/circl v0.0.0-20241113220611-a91ad6141f93/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/openpgp/benchmark_v6_test.go b/openpgp/benchmark_v6_test.go new file mode 100644 index 000000000..c05937654 --- /dev/null +++ b/openpgp/benchmark_v6_test.go @@ -0,0 +1,295 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + "io/ioutil" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const benchmarkMessageSize = 1024 // Signed / encrypted message size in bytes + +var benchmarkTestSet = map[string]*packet.Config{ + "RSA_1024": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 1024, + }, + "RSA_2048": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 2048, + }, + "RSA_3072": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 3072, + }, + "RSA_4096": { + Algorithm: packet.PubKeyAlgoRSA, + RSABits: 4096, + }, + "Ed25519_X25519": { + Algorithm: packet.PubKeyAlgoEd25519, + }, + "Ed448_X448": { + Algorithm: packet.PubKeyAlgoEd448, + }, + "P256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP256, + }, + "P384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP384, + }, + "P521": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveNistP521, + }, + "Brainpool256": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP256, + }, + "Brainpool384": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP384, + }, + "Brainpool512": { + Algorithm: packet.PubKeyAlgoECDSA, + Curve: packet.CurveBrainpoolP512, + }, + "ML-DSA3Ed25519_ML-KEM768X25519": { + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + }, + "ML-DSA5Ed448_ML-KEM1024X448": { + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + }, +} + +func benchmarkGenerateKey(b *testing.B, config *packet.Config) [][]byte { + var serializedEntities [][]byte + config.V6Keys = true + + config.AEADConfig = &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + } + + config.Time = func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + b.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + b.Fatalf("Failed to serialize entity: %s", err) + } + + serializedEntities = append(serializedEntities, serializedEntity.Bytes()) + } + + return serializedEntities +} + +func benchmarkParse(b *testing.B, keys [][]byte) []*Entity { + var parsedKeys []*Entity + + b.ResetTimer() + for n := 0; n < b.N; n++ { + keyring, err := ReadKeyRing(bytes.NewReader(keys[n])) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + parsedKeys = append(parsedKeys, keyring[0]) + } + + return parsedKeys +} + +func benchmarkEncrypt(b *testing.B, keys []*Entity, plaintext []byte, sign bool) [][]byte { + var encryptedMessages [][]byte + + var config = &packet.Config{ + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + V6Keys: true, + } + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + var signed *Entity + if sign { + signed = keys[n%len(keys)] + } + + w, err := Encrypt(buf, EntityList{keys[n%len(keys)]}, signed, nil, config) + if err != nil { + b.Errorf("Failed to initalize encryption: %s", err) + continue + } + + _, err = w.Write(plaintext) + if err != nil { + b.Errorf("Error writing plaintext: %s", err) + continue + } + + err = w.Close() + if err != nil { + b.Errorf("Error closing WriteCloser: %s", err) + continue + } + + encryptedMessages = append(encryptedMessages, buf.Bytes()) + } + + return encryptedMessages +} + +func benchmarkDecrypt(b *testing.B, keys []*Entity, plaintext []byte, encryptedMessages [][]byte, verify bool) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + reader := bytes.NewReader(encryptedMessages[n%len(encryptedMessages)]) + md, err := ReadMessage(reader, EntityList{keys[n%len(keys)]}, nil, nil) + if err != nil { + b.Errorf("Error reading message: %s", err) + continue + } + + decrypted, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + b.Errorf("Error reading encrypted content: %s", err) + continue + } + + if !bytes.Equal(decrypted, plaintext) { + b.Error("Decrypted wrong plaintext") + } + + if verify { + if md.SignatureError != nil { + b.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + b.Error("Signature missing") + } + } + } +} + +func benchmarkSign(b *testing.B, keys []*Entity, plaintext []byte) [][]byte { + var signatures [][]byte + + b.ResetTimer() + for n := 0; n < b.N; n++ { + buf := new(bytes.Buffer) + + err := DetachSign(buf, keys[n%len(keys)], bytes.NewReader(plaintext), nil) + if err != nil { + b.Errorf("Failed to sign: %s", err) + continue + } + + signatures = append(signatures, buf.Bytes()) + } + + return signatures +} + +func benchmarkVerify(b *testing.B, keys []*Entity, plaintext []byte, signatures [][]byte) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + signed := bytes.NewReader(plaintext) + signature := bytes.NewReader(signatures[n%len(signatures)]) + + parsedSignature, signer, signatureError := VerifyDetachedSignature(EntityList{keys[n%len(keys)]}, signed, signature, nil) + + if signatureError != nil { + b.Errorf("Signature error: %s", signatureError) + } + + if parsedSignature == nil { + b.Error("Signature missing") + } + + if signer == nil { + b.Error("Signer missing") + } + } +} + +func BenchmarkV6Keys(b *testing.B) { + serializedKeys := make(map[string][][]byte) + parsedKeys := make(map[string][]*Entity) + encryptedMessages := make(map[string][][]byte) + encryptedSignedMessages := make(map[string][][]byte) + signatures := make(map[string][][]byte) + + var plaintext [benchmarkMessageSize]byte + _, _ = rand.Read(plaintext[:]) + + for name, config := range benchmarkTestSet { + b.Run("Generate "+name, func(b *testing.B) { + serializedKeys[name] = benchmarkGenerateKey(b, config) + b.Logf("Generate %s: %d bytes", name, len(serializedKeys[name][0])) + }) + } + + for name, keys := range serializedKeys { + b.Run("Parse_"+name, func(b *testing.B) { + parsedKeys[name] = benchmarkParse(b, keys) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_"+name, func(b *testing.B) { + encryptedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], false) + b.Logf("Encrypt %s: %d bytes", name, len(encryptedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedMessages[name], false) + }) + } + + for name, keys := range parsedKeys { + b.Run("Encrypt_Sign_"+name, func(b *testing.B) { + encryptedSignedMessages[name] = benchmarkEncrypt(b, keys, plaintext[:], true) + b.Logf("Encrypt_Sign %s: %d bytes", name, len(encryptedSignedMessages[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Decrypt_Verify_"+name, func(b *testing.B) { + benchmarkDecrypt(b, keys, plaintext[:], encryptedSignedMessages[name], true) + }) + } + + for name, keys := range parsedKeys { + b.Run("Sign_"+name, func(b *testing.B) { + signatures[name] = benchmarkSign(b, keys, plaintext[:]) + b.Logf("Sign %s: %d bytes", name, len(signatures[name][0])) + }) + } + + for name, keys := range parsedKeys { + b.Run("Verify_"+name, func(b *testing.B) { + benchmarkVerify(b, keys, plaintext[:], signatures[name]) + }) + } +} diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index aea7f95b6..44a95a1f0 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -206,7 +206,7 @@ func Decode(data []byte) (b *Block, rest []byte) { type dashEscaper struct { buffered *bufio.Writer hashers []hash.Hash // one per key in privateKeys - hashType crypto.Hash + hashType []crypto.Hash toHash io.Writer // writes to all the hashes in hashers salts [][]byte // salts for the signatures if v6 armorHeader map[string]string // Armor headers @@ -328,7 +328,7 @@ func (d *dashEscaper) Close() (err error) { sig.Version = k.Version sig.SigType = packet.SigTypeText sig.PubKeyAlgo = k.PubKeyAlgo - sig.Hash = d.hashType + sig.Hash = d.hashType[i] sig.CreationTime = t sig.IssuerKeyId = &k.KeyId sig.IssuerFingerprint = k.Fingerprint @@ -399,14 +399,16 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config return nil, errors.UnsupportedError("unsupported hash type: " + strconv.Itoa(int(hashType))) } var hashers []hash.Hash + var hashTypes []crypto.Hash var ws []io.Writer var salts [][]byte for _, sk := range privateKeys { - h := hashType.New() + selectedHash := sk.PubKeyAlgo.HandleSpecificHash(hashType) + h := selectedHash.New() if sk.Version == 6 { // generate salt var salt []byte - salt, err = packet.SignatureSaltForHash(hashType, config.Random()) + salt, err = packet.SignatureSaltForHash(selectedHash, config.Random()) if err != nil { return } @@ -416,6 +418,7 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config salts = append(salts, salt) } hashers = append(hashers, h) + hashTypes = append(hashTypes, selectedHash) ws = append(ws, h) } toHash := io.MultiWriter(ws...) @@ -446,7 +449,7 @@ func EncodeMultiWithHeader(w io.Writer, privateKeys []*packet.PrivateKey, config plaintext = &dashEscaper{ buffered: buffered, hashers: hashers, - hashType: hashType, + hashType: hashTypes, toHash: toHash, salts: salts, armorHeader: headers, diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index db8fb163b..85a06b17c 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -12,13 +12,50 @@ import ( "io" "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519" +) + +const ( + KDFVersion1 = 1 + KDFVersionForwarding = 255 ) type KDF struct { - Hash algorithm.Hash - Cipher algorithm.Cipher + Version int // Defaults to v1; 255 for forwarding + Hash algorithm.Hash + Cipher algorithm.Cipher + ReplacementFingerprint []byte // (forwarding only) fingerprint to use instead of recipient's (20 octets) +} + +func (kdf *KDF) Serialize(w io.Writer) (err error) { + switch kdf.Version { + case 0, KDFVersion1: // Default to v1 if unspecified + return kdf.serializeForHash(w) + case KDFVersionForwarding: + // Length || Version || Hash || Cipher || Replacement Fingerprint + length := byte(3 + len(kdf.ReplacementFingerprint)) + if _, err := w.Write([]byte{length, KDFVersionForwarding, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + if _, err := w.Write(kdf.ReplacementFingerprint); err != nil { + return err + } + + return nil + default: + return errors.New("ecdh: invalid KDF version") + } +} + +func (kdf *KDF) serializeForHash(w io.Writer) (err error) { + // Length || Version || Hash || Cipher + if _, err := w.Write([]byte{3, KDFVersion1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil { + return err + } + return nil } type PublicKey struct { @@ -32,13 +69,10 @@ type PrivateKey struct { D []byte } -func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey { +func NewPublicKey(curve ecc.ECDHCurve, kdf KDF) *PublicKey { return &PublicKey{ curve: curve, - KDF: KDF{ - Hash: kdfHash, - Cipher: kdfCipher, - }, + KDF: kdf, } } @@ -149,21 +183,31 @@ func Decrypt(priv *PrivateKey, vsG, c, curveOID, fingerprint []byte) (msg []byte } func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLeading, stripTrailing bool) ([]byte, error) { - // Param = curve_OID_len || curve_OID || public_key_alg_ID || 03 - // || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap + // Param = curve_OID_len || curve_OID || public_key_alg_ID + // || KDF_params for AESKeyWrap // || "Anonymous Sender " || recipient_fingerprint; param := new(bytes.Buffer) if _, err := param.Write(curveOID); err != nil { return nil, err } - algKDF := []byte{18, 3, 1, pub.KDF.Hash.Id(), pub.KDF.Cipher.Id()} - if _, err := param.Write(algKDF); err != nil { + algo := []byte{18} + if _, err := param.Write(algo); err != nil { + return nil, err + } + + if err := pub.KDF.serializeForHash(param); err != nil { return nil, err } + if _, err := param.Write([]byte("Anonymous Sender ")); err != nil { return nil, err } - if _, err := param.Write(fingerprint[:]); err != nil { + + if pub.KDF.ReplacementFingerprint != nil { + fingerprint = pub.KDF.ReplacementFingerprint + } + + if _, err := param.Write(fingerprint); err != nil { return nil, err } @@ -204,3 +248,40 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead func Validate(priv *PrivateKey) error { return priv.curve.ValidateECDH(priv.Point, priv.D) } + +func DeriveProxyParam(recipientKey, forwardeeKey *PrivateKey) (proxyParam []byte, err error) { + if recipientKey.GetCurve().GetCurveName() != "curve25519" { + return nil, pgperrors.InvalidArgumentError("recipient subkey is not curve25519") + } + + if forwardeeKey.GetCurve().GetCurveName() != "curve25519" { + return nil, pgperrors.InvalidArgumentError("forwardee subkey is not curve25519") + } + + c := ecc.NewCurve25519() + + // Clamp and reverse two secrets + proxyParam, err = curve25519.DeriveProxyParam(c.MarshalByteSecret(recipientKey.D), c.MarshalByteSecret(forwardeeKey.D)) + + return proxyParam, err +} + +func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) { + c := ecc.NewCurve25519() + + parsedEphemeral := c.UnmarshalBytePoint(ephemeral) + if parsedEphemeral == nil { + return nil, pgperrors.InvalidArgumentError("invalid ephemeral") + } + + if len(proxyParam) != curve25519.ParamSize { + return nil, pgperrors.InvalidArgumentError("invalid proxy parameter") + } + + transformed, err := curve25519.ProxyTransform(parsedEphemeral, proxyParam) + if err != nil { + return nil, err + } + + return c.MarshalBytePoint(transformed), nil +} diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 1f70b7dd0..0170d776c 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -88,7 +88,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { p := priv.MarshalPoint() d := priv.MarshalByteSecret() - parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF.Hash, priv.KDF.Cipher)) + parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF)) if err := parsed.UnmarshalPoint(p); err != nil { t.Fatalf("unable to unmarshal point: %s", err) @@ -112,3 +112,37 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) { t.Fatal("failed to marshal/unmarshal correctly") } } + +func TestKDFParamsWrite(t *testing.T) { + kdf := KDF{ + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + } + byteBuffer := new(bytes.Buffer) + + testFingerprint := make([]byte, 20) + + expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()} + kdf.Serialize(byteBuffer) + gotBytes := byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV1) { + t.Errorf("error serializing KDF params, got %x, want: %x", gotBytes, expectBytesV1) + } + byteBuffer.Reset() + + kdfV2 := KDF{ + Version: KDFVersionForwarding, + Hash: algorithm.SHA512, + Cipher: algorithm.AES256, + ReplacementFingerprint: testFingerprint, + } + expectBytesV2 := []byte{23, 0xFF, kdfV2.Hash.Id(), kdfV2.Cipher.Id()} + expectBytesV2 = append(expectBytesV2, testFingerprint...) + + kdfV2.Serialize(byteBuffer) + gotBytes = byteBuffer.Bytes() + if !bytes.Equal(gotBytes, expectBytesV2) { + t.Errorf("error serializing KDF params v2, got %x, want: %x", gotBytes, expectBytesV2) + } + byteBuffer.Reset() +} diff --git a/openpgp/errors/errors.go b/openpgp/errors/errors.go index 0eb3937b3..33f8d029a 100644 --- a/openpgp/errors/errors.go +++ b/openpgp/errors/errors.go @@ -73,6 +73,8 @@ func (i InvalidArgumentError) Error() string { return "openpgp: invalid argument: " + string(i) } +var InvalidForwardeeKeyError = InvalidArgumentError("invalid forwardee key") + // SignatureError indicates that a syntactically valid signature failed to // validate. type SignatureError string diff --git a/openpgp/forwarding.go b/openpgp/forwarding.go new file mode 100644 index 000000000..ae45c3c2b --- /dev/null +++ b/openpgp/forwarding.go @@ -0,0 +1,163 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package openpgp + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + i := e.PrimaryIdentity() + if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired + i.SelfSignature.SigExpired(now) || // user ID self-signature has expired + e.Revoked(now) || // primary key has been revoked + i.Revoked(now) { // user ID has been revoked + return nil, nil, errors.InvalidArgumentError("primary key is expired") + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + keyLifetimeSecs := config.KeyLifetime() + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + err = forwardeeKey.addUserId(name, comment, email, config, now, keyLifetimeSecs, true) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + // Filter expiration & revokal + if forwarderSubKey.PublicKey.KeyExpired(forwarderSubKey.Sig, now) || + forwarderSubKey.Sig.SigExpired(now) || + forwarderSubKey.Revoked(now) { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKey.Sig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKey.Sig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKey.Sig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKey.Sig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKey.Sig.FlagForward = true + + // Re-sign subkey binding signature + err = forwardeeSubKey.Sig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go new file mode 100644 index 000000000..7bc167180 --- /dev/null +++ b/openpgp/forwarding_test.go @@ -0,0 +1,224 @@ +package openpgp + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "golang.org/x/crypto/openpgp/armor" +) + +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj +-----END PGP PRIVATE KEY BLOCK-----` + +const forwardedMessage = `-----BEGIN PGP MESSAGE----- + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa +-----END PGP MESSAGE-----` + +const forwardedPlaintext = "Message for Bob" + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) + if err != nil { + t.Error(err) + return + } + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) + if err != nil { + t.Error(err) + return + } + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) + if err != nil { + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingFull(t *testing.T) { + keyConfig := &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + Curve: packet.Curve25519, + } + + plaintext := make([]byte, 1024) + rand.Read(plaintext) + + bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig) + if err != nil { + t.Fatal(err) + } + + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) + } + + if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + _, err = w.Write(plaintext) + if err != nil { + t.Fatal(err) + } + + err = w.Close() + if err != nil { + t.Fatal(err) + } + + encrypted := buf.Bytes() + + // Decrypt message for Bob + m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("decrypted does not match original") + } + + // Forward message + + transformed := transformTestMessage(t, encrypted, instances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } + + // Setup further forwarding + danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { + bytesReader := bytes.NewReader(encrypted) + packets := packet.NewReader(bytesReader) + splitPoint := int64(0) + transformedEncryptedKey := bytes.NewBuffer(nil) + +Loop: + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error in parsing message: %s", err) + } + switch p := p.(type) { + case *packet.EncryptedKey: + tp, err := p.ProxyTransform(instance) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = tp.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + return transformed +} + +func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity { + serializedEntity := bytes.NewBuffer(nil) + err := key.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatalf("Error in serializing forwardee key: %s", err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatalf("Error in reading forwardee key: %s", err) + } + + if len(el) != 1 { + t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el)) + } + + return el[0] +} diff --git a/openpgp/integration_tests/v2/utils_test.go b/openpgp/integration_tests/v2/utils_test.go index 0c3c49c31..bbb7bf003 100644 --- a/openpgp/integration_tests/v2/utils_test.go +++ b/openpgp/integration_tests/v2/utils_test.go @@ -30,10 +30,12 @@ func generateFreshTestVectors(num int) (vectors []testVector, err error) { v = "v6" } pkAlgoNames := map[packet.PublicKeyAlgorithm]string{ - packet.PubKeyAlgoRSA: "rsa_" + v, - packet.PubKeyAlgoEdDSA: "EdDSA_" + v, - packet.PubKeyAlgoEd25519: "ed25519_" + v, - packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoRSA: "rsa_" + v, + packet.PubKeyAlgoEdDSA: "EdDSA_" + v, + packet.PubKeyAlgoEd25519: "ed25519_" + v, + packet.PubKeyAlgoEd448: "ed448_" + v, + packet.PubKeyAlgoMldsa65Ed25519: "mldsa_" + v, + packet.PubKeyAlgoSlhdsaShake128s: "slhdsa128s_" + v, } newVector := testVector{ @@ -238,6 +240,8 @@ func randConfig() *packet.Config { packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoEd25519, packet.PubKeyAlgoEd448, + packet.PubKeyAlgoMldsa65Ed25519, + packet.PubKeyAlgoSlhdsaShake128s, } pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] @@ -268,7 +272,9 @@ func randConfig() *packet.Config { compConf := &packet.CompressionConfig{Level: level} var v6 bool - if mathrand.Int()%2 == 0 { + if pkAlgo == packet.PubKeyAlgoMldsa65Ed25519 || pkAlgo == packet.PubKeyAlgoSlhdsaShake128s { + 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 888767c4e..a6721ff98 100644 --- a/openpgp/internal/ecc/curve25519.go +++ b/openpgp/internal/ecc/curve25519.go @@ -3,10 +3,9 @@ package ecc import ( "crypto/subtle" - "io" - "github.com/ProtonMail/go-crypto/openpgp/errors" x25519lib "github.com/cloudflare/circl/dh/x25519" + "io" ) type curve25519 struct{} diff --git a/openpgp/internal/ecc/curve25519/curve25519.go b/openpgp/internal/ecc/curve25519/curve25519.go new file mode 100644 index 000000000..21670a82c --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519.go @@ -0,0 +1,122 @@ +// Package curve25519 implements custom field operations without clamping for forwarding. +package curve25519 + +import ( + "crypto/subtle" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519/field" + x25519lib "github.com/cloudflare/circl/dh/x25519" + "math/big" +) + +var curveGroupByte = [x25519lib.Size]byte{ + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed, +} + +const ParamSize = x25519lib.Size + +func DeriveProxyParam(recipientSecretByte, forwardeeSecretByte []byte) (proxyParam []byte, err error) { + curveGroup := new(big.Int).SetBytes(curveGroupByte[:]) + recipientSecret := new(big.Int).SetBytes(recipientSecretByte) + forwardeeSecret := new(big.Int).SetBytes(forwardeeSecretByte) + + proxyTransform := new(big.Int).Mod( + new(big.Int).Mul( + new(big.Int).ModInverse(forwardeeSecret, curveGroup), + recipientSecret, + ), + curveGroup, + ) + + rawProxyParam := proxyTransform.Bytes() + + // pad and convert to small endian + proxyParam = make([]byte, x25519lib.Size) + l := len(rawProxyParam) + for i := 0; i < l; i++ { + proxyParam[i] = rawProxyParam[l-i-1] + } + + return proxyParam, nil +} + +func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) { + var transformed, safetyCheck [x25519lib.Size]byte + + var scalarEight = make([]byte, x25519lib.Size) + scalarEight[0] = 0x08 + err := ScalarMult(&safetyCheck, scalarEight, ephemeral) + if err != nil { + return nil, err + } + + err = ScalarMult(&transformed, proxyParam, ephemeral) + if err != nil { + return nil, err + } + + return transformed[:], nil +} + +func ScalarMult(dst *[32]byte, scalar, point []byte) error { + var in, base, zero [32]byte + copy(in[:], scalar) + copy(base[:], point) + + scalarMult(dst, &in, &base) + if subtle.ConstantTimeCompare(dst[:], zero[:]) == 1 { + return errors.InvalidArgumentError("invalid ephemeral: low order point") + } + + return nil +} + +func scalarMult(dst, scalar, point *[32]byte) { + var e [32]byte + + copy(e[:], scalar[:]) + + var x1, x2, z2, x3, z3, tmp0, tmp1 field.Element + x1.SetBytes(point[:]) + x2.One() + x3.Set(&x1) + z3.One() + + swap := 0 + for pos := 254; pos >= 0; pos-- { + b := e[pos/8] >> uint(pos&7) + b &= 1 + swap ^= int(b) + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + swap = int(b) + + tmp0.Subtract(&x3, &z3) + tmp1.Subtract(&x2, &z2) + x2.Add(&x2, &z2) + z2.Add(&x3, &z3) + z3.Multiply(&tmp0, &x2) + z2.Multiply(&z2, &tmp1) + tmp0.Square(&tmp1) + tmp1.Square(&x2) + x3.Add(&z3, &z2) + z2.Subtract(&z3, &z2) + x2.Multiply(&tmp1, &tmp0) + tmp1.Subtract(&tmp1, &tmp0) + z2.Square(&z2) + + z3.Mult32(&tmp1, 121666) + x3.Square(&x3) + tmp0.Add(&tmp0, &z3) + z3.Multiply(&x1, &z2) + z2.Multiply(&tmp1, &tmp0) + } + + x2.Swap(&x3, swap) + z2.Swap(&z3, swap) + + z2.Invert(&z2) + x2.Multiply(&x2, &z2) + copy(dst[:], x2.Bytes()) +} diff --git a/openpgp/internal/ecc/curve25519/curve25519_test.go b/openpgp/internal/ecc/curve25519/curve25519_test.go new file mode 100644 index 000000000..bd82e03e2 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/curve25519_test.go @@ -0,0 +1,89 @@ +// Package curve25519 implements custom field operations without clamping for forwarding. +package curve25519 + +import ( + "bytes" + "encoding/hex" + "testing" +) + +const ( + hexBobSecret = "5989216365053dcf9e35a04b2a1fc19b83328426be6bb7d0a2ae78105e2e3188" + hexCharlesSecret = "684da6225bcd44d880168fc5bec7d2f746217f014c8019005f144cc148f16a00" + hexExpectedProxyParam = "e89786987c3a3ec761a679bc372cd11a425eda72bd5265d78ad0f5f32ee64f02" + + hexMessagePoint = "aaea7b3bb92f5f545d023ccb15b50f84ba1bdd53be7f5cfadcfb0106859bf77e" + hexInputProxyParam = "83c57cbe645a132477af55d5020281305860201608e81a1de43ff83f245fb302" + hexExpectedTransformedPoint = "ec31bb937d7ef08c451d516be1d7976179aa7171eea598370661d1152b85005a" + + hexSmallSubgroupPoint = "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" +) + +func TestDeriveProxyParam(t *testing.T) { + bobSecret, err := hex.DecodeString(hexBobSecret) + if err != nil { + t.Fatalf("Unexpected error in decoding recipient secret: %s", err) + } + + charlesSecret, err := hex.DecodeString(hexCharlesSecret) + if err != nil { + t.Fatalf("Unexpected error in decoding forwardee secret: %s", err) + } + + expectedProxyParam, err := hex.DecodeString(hexExpectedProxyParam) + if err != nil { + t.Fatalf("Unexpected error in parameter decoding expected proxy parameter: %s", err) + } + + proxyParam, err := DeriveProxyParam(bobSecret, charlesSecret) + if err != nil { + t.Fatalf("Unexpected error in parameter derivation: %s", err) + } + + if bytes.Compare(proxyParam, expectedProxyParam) != 0 { + t.Errorf("Computed wrong proxy parameter, expected %x got %x", expectedProxyParam, proxyParam) + } +} + +func TestTransformMessage(t *testing.T) { + proxyParam, err := hex.DecodeString(hexInputProxyParam) + if err != nil { + t.Fatalf("Unexpected error in decoding proxy parameter: %s", err) + } + + messagePoint, err := hex.DecodeString(hexMessagePoint) + if err != nil { + t.Fatalf("Unexpected error in decoding message point: %s", err) + } + + expectedTransformed, err := hex.DecodeString(hexExpectedTransformedPoint) + if err != nil { + t.Fatalf("Unexpected error in parameter decoding expected transformed point: %s", err) + } + + transformed, err := ProxyTransform(messagePoint, proxyParam) + if err != nil { + t.Fatalf("Unexpected error in parameter derivation: %s", err) + } + + if bytes.Compare(transformed, expectedTransformed) != 0 { + t.Errorf("Computed wrong proxy parameter, expected %x got %x", expectedTransformed, transformed) + } +} + +func TestTransformSmallSubgroup(t *testing.T) { + proxyParam, err := hex.DecodeString(hexInputProxyParam) + if err != nil { + t.Fatalf("Unexpected error in decoding proxy parameter: %s", err) + } + + messagePoint, err := hex.DecodeString(hexSmallSubgroupPoint) + if err != nil { + t.Fatalf("Unexpected error in decoding small sugroup point: %s", err) + } + + _, err = ProxyTransform(messagePoint, proxyParam) + if err == nil { + t.Error("Expected small subgroup error") + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe.go b/openpgp/internal/ecc/curve25519/field/fe.go new file mode 100644 index 000000000..ca841ad99 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe.go @@ -0,0 +1,416 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package field implements fast arithmetic modulo 2^255-19. +package field + +import ( + "crypto/subtle" + "encoding/binary" + "math/bits" +) + +// Element represents an element of the field GF(2^255-19). Note that this +// is not a cryptographically secure group, and should only be used to interact +// with edwards25519.Point coordinates. +// +// This type works similarly to math/big.Int, and all arguments and receivers +// are allowed to alias. +// +// The zero value is a valid zero element. +type Element struct { + // An element t represents the integer + // t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204 + // + // Between operations, all limbs are expected to be lower than 2^52. + l0 uint64 + l1 uint64 + l2 uint64 + l3 uint64 + l4 uint64 +} + +const maskLow51Bits uint64 = (1 << 51) - 1 + +var feZero = &Element{0, 0, 0, 0, 0} + +// Zero sets v = 0, and returns v. +func (v *Element) Zero() *Element { + *v = *feZero + return v +} + +var feOne = &Element{1, 0, 0, 0, 0} + +// One sets v = 1, and returns v. +func (v *Element) One() *Element { + *v = *feOne + return v +} + +// reduce reduces v modulo 2^255 - 19 and returns it. +func (v *Element) reduce() *Element { + v.carryPropagate() + + // After the light reduction we now have a field element representation + // v < 2^255 + 2^13 * 19, but need v < 2^255 - 19. + + // If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1, + // generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise. + c := (v.l0 + 19) >> 51 + c = (v.l1 + c) >> 51 + c = (v.l2 + c) >> 51 + c = (v.l3 + c) >> 51 + c = (v.l4 + c) >> 51 + + // If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's + // effectively applying the reduction identity to the carry. + v.l0 += 19 * c + + v.l1 += v.l0 >> 51 + v.l0 = v.l0 & maskLow51Bits + v.l2 += v.l1 >> 51 + v.l1 = v.l1 & maskLow51Bits + v.l3 += v.l2 >> 51 + v.l2 = v.l2 & maskLow51Bits + v.l4 += v.l3 >> 51 + v.l3 = v.l3 & maskLow51Bits + // no additional carry + v.l4 = v.l4 & maskLow51Bits + + return v +} + +// Add sets v = a + b, and returns v. +func (v *Element) Add(a, b *Element) *Element { + v.l0 = a.l0 + b.l0 + v.l1 = a.l1 + b.l1 + v.l2 = a.l2 + b.l2 + v.l3 = a.l3 + b.l3 + v.l4 = a.l4 + b.l4 + // Using the generic implementation here is actually faster than the + // assembly. Probably because the body of this function is so simple that + // the compiler can figure out better optimizations by inlining the carry + // propagation. TODO + return v.carryPropagateGeneric() +} + +// Subtract sets v = a - b, and returns v. +func (v *Element) Subtract(a, b *Element) *Element { + // We first add 2 * p, to guarantee the subtraction won't underflow, and + // then subtract b (which can be up to 2^255 + 2^13 * 19). + v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0 + v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1 + v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2 + v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3 + v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4 + return v.carryPropagate() +} + +// Negate sets v = -a, and returns v. +func (v *Element) Negate(a *Element) *Element { + return v.Subtract(feZero, a) +} + +// Invert sets v = 1/z mod p, and returns v. +// +// If z == 0, Invert returns v = 0. +func (v *Element) Invert(z *Element) *Element { + // Inversion is implemented as exponentiation with exponent p − 2. It uses the + // same sequence of 255 squarings and 11 multiplications as [Curve25519]. + var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element + + z2.Square(z) // 2 + t.Square(&z2) // 4 + t.Square(&t) // 8 + z9.Multiply(&t, z) // 9 + z11.Multiply(&z9, &z2) // 11 + t.Square(&z11) // 22 + z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0 + + t.Square(&z2_5_0) // 2^6 - 2^1 + for i := 0; i < 4; i++ { + t.Square(&t) // 2^10 - 2^5 + } + z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0 + + t.Square(&z2_10_0) // 2^11 - 2^1 + for i := 0; i < 9; i++ { + t.Square(&t) // 2^20 - 2^10 + } + z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0 + + t.Square(&z2_20_0) // 2^21 - 2^1 + for i := 0; i < 19; i++ { + t.Square(&t) // 2^40 - 2^20 + } + t.Multiply(&t, &z2_20_0) // 2^40 - 2^0 + + t.Square(&t) // 2^41 - 2^1 + for i := 0; i < 9; i++ { + t.Square(&t) // 2^50 - 2^10 + } + z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0 + + t.Square(&z2_50_0) // 2^51 - 2^1 + for i := 0; i < 49; i++ { + t.Square(&t) // 2^100 - 2^50 + } + z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0 + + t.Square(&z2_100_0) // 2^101 - 2^1 + for i := 0; i < 99; i++ { + t.Square(&t) // 2^200 - 2^100 + } + t.Multiply(&t, &z2_100_0) // 2^200 - 2^0 + + t.Square(&t) // 2^201 - 2^1 + for i := 0; i < 49; i++ { + t.Square(&t) // 2^250 - 2^50 + } + t.Multiply(&t, &z2_50_0) // 2^250 - 2^0 + + t.Square(&t) // 2^251 - 2^1 + t.Square(&t) // 2^252 - 2^2 + t.Square(&t) // 2^253 - 2^3 + t.Square(&t) // 2^254 - 2^4 + t.Square(&t) // 2^255 - 2^5 + + return v.Multiply(&t, &z11) // 2^255 - 21 +} + +// Set sets v = a, and returns v. +func (v *Element) Set(a *Element) *Element { + *v = *a + return v +} + +// SetBytes sets v to x, which must be a 32-byte little-endian encoding. +// +// Consistent with RFC 7748, the most significant bit (the high bit of the +// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1) +// are accepted. Note that this is laxer than specified by RFC 8032. +func (v *Element) SetBytes(x []byte) *Element { + if len(x) != 32 { + panic("edwards25519: invalid field element input size") + } + + // Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51). + v.l0 = binary.LittleEndian.Uint64(x[0:8]) + v.l0 &= maskLow51Bits + // Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51). + v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3 + v.l1 &= maskLow51Bits + // Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51). + v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6 + v.l2 &= maskLow51Bits + // Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51). + v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1 + v.l3 &= maskLow51Bits + // Bits 204:251 (bytes 24:32, bits 192:256, shift 12, mask 51). + // Note: not bytes 25:33, shift 4, to avoid overread. + v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12 + v.l4 &= maskLow51Bits + + return v +} + +// Bytes returns the canonical 32-byte little-endian encoding of v. +func (v *Element) Bytes() []byte { + // This function is outlined to make the allocations inline in the caller + // rather than happen on the heap. + var out [32]byte + return v.bytes(&out) +} + +func (v *Element) bytes(out *[32]byte) []byte { + t := *v + t.reduce() + + var buf [8]byte + for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} { + bitsOffset := i * 51 + binary.LittleEndian.PutUint64(buf[:], l<= len(out) { + break + } + out[off] |= bb + } + } + + return out[:] +} + +// Equal returns 1 if v and u are equal, and 0 otherwise. +func (v *Element) Equal(u *Element) int { + sa, sv := u.Bytes(), v.Bytes() + return subtle.ConstantTimeCompare(sa, sv) +} + +// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise. +func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) } + +// Select sets v to a if cond == 1, and to b if cond == 0. +func (v *Element) Select(a, b *Element, cond int) *Element { + m := mask64Bits(cond) + v.l0 = (m & a.l0) | (^m & b.l0) + v.l1 = (m & a.l1) | (^m & b.l1) + v.l2 = (m & a.l2) | (^m & b.l2) + v.l3 = (m & a.l3) | (^m & b.l3) + v.l4 = (m & a.l4) | (^m & b.l4) + return v +} + +// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v. +func (v *Element) Swap(u *Element, cond int) { + m := mask64Bits(cond) + t := m & (v.l0 ^ u.l0) + v.l0 ^= t + u.l0 ^= t + t = m & (v.l1 ^ u.l1) + v.l1 ^= t + u.l1 ^= t + t = m & (v.l2 ^ u.l2) + v.l2 ^= t + u.l2 ^= t + t = m & (v.l3 ^ u.l3) + v.l3 ^= t + u.l3 ^= t + t = m & (v.l4 ^ u.l4) + v.l4 ^= t + u.l4 ^= t +} + +// IsNegative returns 1 if v is negative, and 0 otherwise. +func (v *Element) IsNegative() int { + return int(v.Bytes()[0] & 1) +} + +// Absolute sets v to |u|, and returns v. +func (v *Element) Absolute(u *Element) *Element { + return v.Select(new(Element).Negate(u), u, u.IsNegative()) +} + +// Multiply sets v = x * y, and returns v. +func (v *Element) Multiply(x, y *Element) *Element { + feMul(v, x, y) + return v +} + +// Square sets v = x * x, and returns v. +func (v *Element) Square(x *Element) *Element { + feSquare(v, x) + return v +} + +// Mult32 sets v = x * y, and returns v. +func (v *Element) Mult32(x *Element, y uint32) *Element { + x0lo, x0hi := mul51(x.l0, y) + x1lo, x1hi := mul51(x.l1, y) + x2lo, x2hi := mul51(x.l2, y) + x3lo, x3hi := mul51(x.l3, y) + x4lo, x4hi := mul51(x.l4, y) + v.l0 = x0lo + 19*x4hi // carried over per the reduction identity + v.l1 = x1lo + x0hi + v.l2 = x2lo + x1hi + v.l3 = x3lo + x2hi + v.l4 = x4lo + x3hi + // The hi portions are going to be only 32 bits, plus any previous excess, + // so we can skip the carry propagation. + return v +} + +// mul51 returns lo + hi * 2⁵¹ = a * b. +func mul51(a uint64, b uint32) (lo uint64, hi uint64) { + mh, ml := bits.Mul64(a, uint64(b)) + lo = ml & maskLow51Bits + hi = (mh << 13) | (ml >> 51) + return +} + +// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3. +func (v *Element) Pow22523(x *Element) *Element { + var t0, t1, t2 Element + + t0.Square(x) // x^2 + t1.Square(&t0) // x^4 + t1.Square(&t1) // x^8 + t1.Multiply(x, &t1) // x^9 + t0.Multiply(&t0, &t1) // x^11 + t0.Square(&t0) // x^22 + t0.Multiply(&t1, &t0) // x^31 + t1.Square(&t0) // x^62 + for i := 1; i < 5; i++ { // x^992 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1 + t1.Square(&t0) // 2^11 - 2 + for i := 1; i < 10; i++ { // 2^20 - 2^10 + t1.Square(&t1) + } + t1.Multiply(&t1, &t0) // 2^20 - 1 + t2.Square(&t1) // 2^21 - 2 + for i := 1; i < 20; i++ { // 2^40 - 2^20 + t2.Square(&t2) + } + t1.Multiply(&t2, &t1) // 2^40 - 1 + t1.Square(&t1) // 2^41 - 2 + for i := 1; i < 10; i++ { // 2^50 - 2^10 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // 2^50 - 1 + t1.Square(&t0) // 2^51 - 2 + for i := 1; i < 50; i++ { // 2^100 - 2^50 + t1.Square(&t1) + } + t1.Multiply(&t1, &t0) // 2^100 - 1 + t2.Square(&t1) // 2^101 - 2 + for i := 1; i < 100; i++ { // 2^200 - 2^100 + t2.Square(&t2) + } + t1.Multiply(&t2, &t1) // 2^200 - 1 + t1.Square(&t1) // 2^201 - 2 + for i := 1; i < 50; i++ { // 2^250 - 2^50 + t1.Square(&t1) + } + t0.Multiply(&t1, &t0) // 2^250 - 1 + t0.Square(&t0) // 2^251 - 2 + t0.Square(&t0) // 2^252 - 4 + return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3) +} + +// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion. +var sqrtM1 = &Element{1718705420411056, 234908883556509, + 2233514472574048, 2117202627021982, 765476049583133} + +// SqrtRatio sets r to the non-negative square root of the ratio of u and v. +// +// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio +// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00, +// and returns r and 0. +func (r *Element) SqrtRatio(u, v *Element) (rr *Element, wasSquare int) { + var a, b Element + + // r = (u * v3) * (u * v7)^((p-5)/8) + v2 := a.Square(v) + uv3 := b.Multiply(u, b.Multiply(v2, v)) + uv7 := a.Multiply(uv3, a.Square(v2)) + r.Multiply(uv3, r.Pow22523(uv7)) + + check := a.Multiply(v, a.Square(r)) // check = v * r^2 + + uNeg := b.Negate(u) + correctSignSqrt := check.Equal(u) + flippedSignSqrt := check.Equal(uNeg) + flippedSignSqrtI := check.Equal(uNeg.Multiply(uNeg, sqrtM1)) + + rPrime := b.Multiply(r, sqrtM1) // r_prime = SQRT_M1 * r + // r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r) + r.Select(rPrime, r, flippedSignSqrt|flippedSignSqrtI) + + r.Absolute(r) // Choose the nonnegative square root. + return r, correctSignSqrt | flippedSignSqrt +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_alias_test.go b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go new file mode 100644 index 000000000..64e57c4f3 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_alias_test.go @@ -0,0 +1,126 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import ( + "testing" + "testing/quick" +) + +func checkAliasingOneArg(f func(v, x *Element) *Element) func(v, x Element) bool { + return func(v, x Element) bool { + x1, v1 := x, x + + // Calculate a reference f(x) without aliasing. + if out := f(&v, &x); out != &v && isInBounds(out) { + return false + } + + // Test aliasing the argument and the receiver. + if out := f(&v1, &v1); out != &v1 || v1 != v { + return false + } + + // Ensure the arguments was not modified. + return x == x1 + } +} + +func checkAliasingTwoArgs(f func(v, x, y *Element) *Element) func(v, x, y Element) bool { + return func(v, x, y Element) bool { + x1, y1, v1 := x, y, Element{} + + // Calculate a reference f(x, y) without aliasing. + if out := f(&v, &x, &y); out != &v && isInBounds(out) { + return false + } + + // Test aliasing the first argument and the receiver. + v1 = x + if out := f(&v1, &v1, &y); out != &v1 || v1 != v { + return false + } + // Test aliasing the second argument and the receiver. + v1 = y + if out := f(&v1, &x, &v1); out != &v1 || v1 != v { + return false + } + + // Calculate a reference f(x, x) without aliasing. + if out := f(&v, &x, &x); out != &v { + return false + } + + // Test aliasing the first argument and the receiver. + v1 = x + if out := f(&v1, &v1, &x); out != &v1 || v1 != v { + return false + } + // Test aliasing the second argument and the receiver. + v1 = x + if out := f(&v1, &x, &v1); out != &v1 || v1 != v { + return false + } + // Test aliasing both arguments and the receiver. + v1 = x + if out := f(&v1, &v1, &v1); out != &v1 || v1 != v { + return false + } + + // Ensure the arguments were not modified. + return x == x1 && y == y1 + } +} + +// TestAliasing checks that receivers and arguments can alias each other without +// leading to incorrect results. That is, it ensures that it's safe to write +// +// v.Invert(v) +// +// or +// +// v.Add(v, v) +// +// without any of the inputs getting clobbered by the output being written. +func TestAliasing(t *testing.T) { + type target struct { + name string + oneArgF func(v, x *Element) *Element + twoArgsF func(v, x, y *Element) *Element + } + for _, tt := range []target{ + {name: "Absolute", oneArgF: (*Element).Absolute}, + {name: "Invert", oneArgF: (*Element).Invert}, + {name: "Negate", oneArgF: (*Element).Negate}, + {name: "Set", oneArgF: (*Element).Set}, + {name: "Square", oneArgF: (*Element).Square}, + {name: "Multiply", twoArgsF: (*Element).Multiply}, + {name: "Add", twoArgsF: (*Element).Add}, + {name: "Subtract", twoArgsF: (*Element).Subtract}, + { + name: "Select0", + twoArgsF: func(v, x, y *Element) *Element { + return (*Element).Select(v, x, y, 0) + }, + }, + { + name: "Select1", + twoArgsF: func(v, x, y *Element) *Element { + return (*Element).Select(v, x, y, 1) + }, + }, + } { + var err error + switch { + case tt.oneArgF != nil: + err = quick.Check(checkAliasingOneArg(tt.oneArgF), &quick.Config{MaxCountScale: 1 << 8}) + case tt.twoArgsF != nil: + err = quick.Check(checkAliasingTwoArgs(tt.twoArgsF), &quick.Config{MaxCountScale: 1 << 8}) + } + if err != nil { + t.Errorf("%v: %v", tt.name, err) + } + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.go b/openpgp/internal/ecc/curve25519/field/fe_amd64.go new file mode 100644 index 000000000..edcf163c4 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.go @@ -0,0 +1,16 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +//go:build amd64 && gc && !purego +// +build amd64,gc,!purego + +package field + +// feMul sets out = a * b. It works like feMulGeneric. +// +//go:noescape +func feMul(out *Element, a *Element, b *Element) + +// feSquare sets out = a * a. It works like feSquareGeneric. +// +//go:noescape +func feSquare(out *Element, a *Element) diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64.s b/openpgp/internal/ecc/curve25519/field/fe_amd64.s new file mode 100644 index 000000000..293f013c9 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64.s @@ -0,0 +1,379 @@ +// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT. + +//go:build amd64 && gc && !purego +// +build amd64,gc,!purego + +#include "textflag.h" + +// func feMul(out *Element, a *Element, b *Element) +TEXT ·feMul(SB), NOSPLIT, $0-24 + MOVQ a+8(FP), CX + MOVQ b+16(FP), BX + + // r0 = a0×b0 + MOVQ (CX), AX + MULQ (BX) + MOVQ AX, DI + MOVQ DX, SI + + // r0 += 19×a1×b4 + MOVQ 8(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a2×b3 + MOVQ 16(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a3×b2 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 16(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r0 += 19×a4×b1 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 8(BX) + ADDQ AX, DI + ADCQ DX, SI + + // r1 = a0×b1 + MOVQ (CX), AX + MULQ 8(BX) + MOVQ AX, R9 + MOVQ DX, R8 + + // r1 += a1×b0 + MOVQ 8(CX), AX + MULQ (BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a2×b4 + MOVQ 16(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a3×b3 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r1 += 19×a4×b2 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 16(BX) + ADDQ AX, R9 + ADCQ DX, R8 + + // r2 = a0×b2 + MOVQ (CX), AX + MULQ 16(BX) + MOVQ AX, R11 + MOVQ DX, R10 + + // r2 += a1×b1 + MOVQ 8(CX), AX + MULQ 8(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += a2×b0 + MOVQ 16(CX), AX + MULQ (BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += 19×a3×b4 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r2 += 19×a4×b3 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(BX) + ADDQ AX, R11 + ADCQ DX, R10 + + // r3 = a0×b3 + MOVQ (CX), AX + MULQ 24(BX) + MOVQ AX, R13 + MOVQ DX, R12 + + // r3 += a1×b2 + MOVQ 8(CX), AX + MULQ 16(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += a2×b1 + MOVQ 16(CX), AX + MULQ 8(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += a3×b0 + MOVQ 24(CX), AX + MULQ (BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r3 += 19×a4×b4 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(BX) + ADDQ AX, R13 + ADCQ DX, R12 + + // r4 = a0×b4 + MOVQ (CX), AX + MULQ 32(BX) + MOVQ AX, R15 + MOVQ DX, R14 + + // r4 += a1×b3 + MOVQ 8(CX), AX + MULQ 24(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a2×b2 + MOVQ 16(CX), AX + MULQ 16(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a3×b1 + MOVQ 24(CX), AX + MULQ 8(BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // r4 += a4×b0 + MOVQ 32(CX), AX + MULQ (BX) + ADDQ AX, R15 + ADCQ DX, R14 + + // First reduction chain + MOVQ $0x0007ffffffffffff, AX + SHLQ $0x0d, DI, SI + SHLQ $0x0d, R9, R8 + SHLQ $0x0d, R11, R10 + SHLQ $0x0d, R13, R12 + SHLQ $0x0d, R15, R14 + ANDQ AX, DI + IMUL3Q $0x13, R14, R14 + ADDQ R14, DI + ANDQ AX, R9 + ADDQ SI, R9 + ANDQ AX, R11 + ADDQ R8, R11 + ANDQ AX, R13 + ADDQ R10, R13 + ANDQ AX, R15 + ADDQ R12, R15 + + // Second reduction chain (carryPropagate) + MOVQ DI, SI + SHRQ $0x33, SI + MOVQ R9, R8 + SHRQ $0x33, R8 + MOVQ R11, R10 + SHRQ $0x33, R10 + MOVQ R13, R12 + SHRQ $0x33, R12 + MOVQ R15, R14 + SHRQ $0x33, R14 + ANDQ AX, DI + IMUL3Q $0x13, R14, R14 + ADDQ R14, DI + ANDQ AX, R9 + ADDQ SI, R9 + ANDQ AX, R11 + ADDQ R8, R11 + ANDQ AX, R13 + ADDQ R10, R13 + ANDQ AX, R15 + ADDQ R12, R15 + + // Store output + MOVQ out+0(FP), AX + MOVQ DI, (AX) + MOVQ R9, 8(AX) + MOVQ R11, 16(AX) + MOVQ R13, 24(AX) + MOVQ R15, 32(AX) + RET + +// func feSquare(out *Element, a *Element) +TEXT ·feSquare(SB), NOSPLIT, $0-16 + MOVQ a+8(FP), CX + + // r0 = l0×l0 + MOVQ (CX), AX + MULQ (CX) + MOVQ AX, SI + MOVQ DX, BX + + // r0 += 38×l1×l4 + MOVQ 8(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, SI + ADCQ DX, BX + + // r0 += 38×l2×l3 + MOVQ 16(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 24(CX) + ADDQ AX, SI + ADCQ DX, BX + + // r1 = 2×l0×l1 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 8(CX) + MOVQ AX, R8 + MOVQ DX, DI + + // r1 += 38×l2×l4 + MOVQ 16(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, R8 + ADCQ DX, DI + + // r1 += 19×l3×l3 + MOVQ 24(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 24(CX) + ADDQ AX, R8 + ADCQ DX, DI + + // r2 = 2×l0×l2 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 16(CX) + MOVQ AX, R10 + MOVQ DX, R9 + + // r2 += l1×l1 + MOVQ 8(CX), AX + MULQ 8(CX) + ADDQ AX, R10 + ADCQ DX, R9 + + // r2 += 38×l3×l4 + MOVQ 24(CX), AX + IMUL3Q $0x26, AX, AX + MULQ 32(CX) + ADDQ AX, R10 + ADCQ DX, R9 + + // r3 = 2×l0×l3 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 24(CX) + MOVQ AX, R12 + MOVQ DX, R11 + + // r3 += 2×l1×l2 + MOVQ 8(CX), AX + IMUL3Q $0x02, AX, AX + MULQ 16(CX) + ADDQ AX, R12 + ADCQ DX, R11 + + // r3 += 19×l4×l4 + MOVQ 32(CX), AX + IMUL3Q $0x13, AX, AX + MULQ 32(CX) + ADDQ AX, R12 + ADCQ DX, R11 + + // r4 = 2×l0×l4 + MOVQ (CX), AX + SHLQ $0x01, AX + MULQ 32(CX) + MOVQ AX, R14 + MOVQ DX, R13 + + // r4 += 2×l1×l3 + MOVQ 8(CX), AX + IMUL3Q $0x02, AX, AX + MULQ 24(CX) + ADDQ AX, R14 + ADCQ DX, R13 + + // r4 += l2×l2 + MOVQ 16(CX), AX + MULQ 16(CX) + ADDQ AX, R14 + ADCQ DX, R13 + + // First reduction chain + MOVQ $0x0007ffffffffffff, AX + SHLQ $0x0d, SI, BX + SHLQ $0x0d, R8, DI + SHLQ $0x0d, R10, R9 + SHLQ $0x0d, R12, R11 + SHLQ $0x0d, R14, R13 + ANDQ AX, SI + IMUL3Q $0x13, R13, R13 + ADDQ R13, SI + ANDQ AX, R8 + ADDQ BX, R8 + ANDQ AX, R10 + ADDQ DI, R10 + ANDQ AX, R12 + ADDQ R9, R12 + ANDQ AX, R14 + ADDQ R11, R14 + + // Second reduction chain (carryPropagate) + MOVQ SI, BX + SHRQ $0x33, BX + MOVQ R8, DI + SHRQ $0x33, DI + MOVQ R10, R9 + SHRQ $0x33, R9 + MOVQ R12, R11 + SHRQ $0x33, R11 + MOVQ R14, R13 + SHRQ $0x33, R13 + ANDQ AX, SI + IMUL3Q $0x13, R13, R13 + ADDQ R13, SI + ANDQ AX, R8 + ADDQ BX, R8 + ANDQ AX, R10 + ADDQ DI, R10 + ANDQ AX, R12 + ADDQ R9, R12 + ANDQ AX, R14 + ADDQ R11, R14 + + // Store output + MOVQ out+0(FP), AX + MOVQ SI, (AX) + MOVQ R8, 8(AX) + MOVQ R10, 16(AX) + MOVQ R12, 24(AX) + MOVQ R14, 32(AX) + RET diff --git a/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go b/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go new file mode 100644 index 000000000..ddb6c9b8f --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_amd64_noasm.go @@ -0,0 +1,12 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !amd64 || !gc || purego +// +build !amd64 !gc purego + +package field + +func feMul(v, x, y *Element) { feMulGeneric(v, x, y) } + +func feSquare(v, x *Element) { feSquareGeneric(v, x) } diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64.go b/openpgp/internal/ecc/curve25519/field/fe_arm64.go new file mode 100644 index 000000000..af459ef51 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64.go @@ -0,0 +1,16 @@ +// Copyright (c) 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build arm64 && gc && !purego +// +build arm64,gc,!purego + +package field + +//go:noescape +func carryPropagate(v *Element) + +func (v *Element) carryPropagate() *Element { + carryPropagate(v) + return v +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64.s b/openpgp/internal/ecc/curve25519/field/fe_arm64.s new file mode 100644 index 000000000..5c91e4589 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64.s @@ -0,0 +1,43 @@ +// Copyright (c) 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build arm64 && gc && !purego +// +build arm64,gc,!purego + +#include "textflag.h" + +// carryPropagate works exactly like carryPropagateGeneric and uses the +// same AND, ADD, and LSR+MADD instructions emitted by the compiler, but +// avoids loading R0-R4 twice and uses LDP and STP. +// +// See https://golang.org/issues/43145 for the main compiler issue. +// +// func carryPropagate(v *Element) +TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8 + MOVD v+0(FP), R20 + + LDP 0(R20), (R0, R1) + LDP 16(R20), (R2, R3) + MOVD 32(R20), R4 + + AND $0x7ffffffffffff, R0, R10 + AND $0x7ffffffffffff, R1, R11 + AND $0x7ffffffffffff, R2, R12 + AND $0x7ffffffffffff, R3, R13 + AND $0x7ffffffffffff, R4, R14 + + ADD R0>>51, R11, R11 + ADD R1>>51, R12, R12 + ADD R2>>51, R13, R13 + ADD R3>>51, R14, R14 + // R4>>51 * 19 + R10 -> R10 + LSR $51, R4, R21 + MOVD $19, R22 + MADD R22, R10, R21, R10 + + STP (R10, R11), 0(R20) + STP (R12, R13), 16(R20) + MOVD R14, 32(R20) + + RET diff --git a/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go b/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go new file mode 100644 index 000000000..234a5b2e5 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_arm64_noasm.go @@ -0,0 +1,12 @@ +// Copyright (c) 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !arm64 || !gc || purego +// +build !arm64 !gc purego + +package field + +func (v *Element) carryPropagate() *Element { + return v.carryPropagateGeneric() +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_bench_test.go b/openpgp/internal/ecc/curve25519/field/fe_bench_test.go new file mode 100644 index 000000000..77dc06cf9 --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_bench_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import "testing" + +func BenchmarkAdd(b *testing.B) { + var x, y Element + x.One() + y.Add(feOne, feOne) + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Add(&x, &y) + } +} + +func BenchmarkMultiply(b *testing.B) { + var x, y Element + x.One() + y.Add(feOne, feOne) + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Multiply(&x, &y) + } +} + +func BenchmarkMult32(b *testing.B) { + var x Element + x.One() + b.ResetTimer() + for i := 0; i < b.N; i++ { + x.Mult32(&x, 0xaa42aa42) + } +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_generic.go b/openpgp/internal/ecc/curve25519/field/fe_generic.go new file mode 100644 index 000000000..7b5b78cbd --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_generic.go @@ -0,0 +1,264 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import "math/bits" + +// uint128 holds a 128-bit number as two 64-bit limbs, for use with the +// bits.Mul64 and bits.Add64 intrinsics. +type uint128 struct { + lo, hi uint64 +} + +// mul64 returns a * b. +func mul64(a, b uint64) uint128 { + hi, lo := bits.Mul64(a, b) + return uint128{lo, hi} +} + +// addMul64 returns v + a * b. +func addMul64(v uint128, a, b uint64) uint128 { + hi, lo := bits.Mul64(a, b) + lo, c := bits.Add64(lo, v.lo, 0) + hi, _ = bits.Add64(hi, v.hi, c) + return uint128{lo, hi} +} + +// shiftRightBy51 returns a >> 51. a is assumed to be at most 115 bits. +func shiftRightBy51(a uint128) uint64 { + return (a.hi << (64 - 51)) | (a.lo >> 51) +} + +func feMulGeneric(v, a, b *Element) { + a0 := a.l0 + a1 := a.l1 + a2 := a.l2 + a3 := a.l3 + a4 := a.l4 + + b0 := b.l0 + b1 := b.l1 + b2 := b.l2 + b3 := b.l3 + b4 := b.l4 + + // Limb multiplication works like pen-and-paper columnar multiplication, but + // with 51-bit limbs instead of digits. + // + // a4 a3 a2 a1 a0 x + // b4 b3 b2 b1 b0 = + // ------------------------ + // a4b0 a3b0 a2b0 a1b0 a0b0 + + // a4b1 a3b1 a2b1 a1b1 a0b1 + + // a4b2 a3b2 a2b2 a1b2 a0b2 + + // a4b3 a3b3 a2b3 a1b3 a0b3 + + // a4b4 a3b4 a2b4 a1b4 a0b4 = + // ---------------------------------------------- + // r8 r7 r6 r5 r4 r3 r2 r1 r0 + // + // We can then use the reduction identity (a * 2²⁵⁵ + b = a * 19 + b) to + // reduce the limbs that would overflow 255 bits. r5 * 2²⁵⁵ becomes 19 * r5, + // r6 * 2³⁰⁶ becomes 19 * r6 * 2⁵¹, etc. + // + // Reduction can be carried out simultaneously to multiplication. For + // example, we do not compute r5: whenever the result of a multiplication + // belongs to r5, like a1b4, we multiply it by 19 and add the result to r0. + // + // a4b0 a3b0 a2b0 a1b0 a0b0 + + // a3b1 a2b1 a1b1 a0b1 19×a4b1 + + // a2b2 a1b2 a0b2 19×a4b2 19×a3b2 + + // a1b3 a0b3 19×a4b3 19×a3b3 19×a2b3 + + // a0b4 19×a4b4 19×a3b4 19×a2b4 19×a1b4 = + // -------------------------------------- + // r4 r3 r2 r1 r0 + // + // Finally we add up the columns into wide, overlapping limbs. + + a1_19 := a1 * 19 + a2_19 := a2 * 19 + a3_19 := a3 * 19 + a4_19 := a4 * 19 + + // r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1) + r0 := mul64(a0, b0) + r0 = addMul64(r0, a1_19, b4) + r0 = addMul64(r0, a2_19, b3) + r0 = addMul64(r0, a3_19, b2) + r0 = addMul64(r0, a4_19, b1) + + // r1 = a0×b1 + a1×b0 + 19×(a2×b4 + a3×b3 + a4×b2) + r1 := mul64(a0, b1) + r1 = addMul64(r1, a1, b0) + r1 = addMul64(r1, a2_19, b4) + r1 = addMul64(r1, a3_19, b3) + r1 = addMul64(r1, a4_19, b2) + + // r2 = a0×b2 + a1×b1 + a2×b0 + 19×(a3×b4 + a4×b3) + r2 := mul64(a0, b2) + r2 = addMul64(r2, a1, b1) + r2 = addMul64(r2, a2, b0) + r2 = addMul64(r2, a3_19, b4) + r2 = addMul64(r2, a4_19, b3) + + // r3 = a0×b3 + a1×b2 + a2×b1 + a3×b0 + 19×a4×b4 + r3 := mul64(a0, b3) + r3 = addMul64(r3, a1, b2) + r3 = addMul64(r3, a2, b1) + r3 = addMul64(r3, a3, b0) + r3 = addMul64(r3, a4_19, b4) + + // r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0 + r4 := mul64(a0, b4) + r4 = addMul64(r4, a1, b3) + r4 = addMul64(r4, a2, b2) + r4 = addMul64(r4, a3, b1) + r4 = addMul64(r4, a4, b0) + + // After the multiplication, we need to reduce (carry) the five coefficients + // to obtain a result with limbs that are at most slightly larger than 2⁵¹, + // to respect the Element invariant. + // + // Overall, the reduction works the same as carryPropagate, except with + // wider inputs: we take the carry for each coefficient by shifting it right + // by 51, and add it to the limb above it. The top carry is multiplied by 19 + // according to the reduction identity and added to the lowest limb. + // + // The largest coefficient (r0) will be at most 111 bits, which guarantees + // that all carries are at most 111 - 51 = 60 bits, which fits in a uint64. + // + // r0 = a0×b0 + 19×(a1×b4 + a2×b3 + a3×b2 + a4×b1) + // r0 < 2⁵²×2⁵² + 19×(2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵² + 2⁵²×2⁵²) + // r0 < (1 + 19 × 4) × 2⁵² × 2⁵² + // r0 < 2⁷ × 2⁵² × 2⁵² + // r0 < 2¹¹¹ + // + // Moreover, the top coefficient (r4) is at most 107 bits, so c4 is at most + // 56 bits, and c4 * 19 is at most 61 bits, which again fits in a uint64 and + // allows us to easily apply the reduction identity. + // + // r4 = a0×b4 + a1×b3 + a2×b2 + a3×b1 + a4×b0 + // r4 < 5 × 2⁵² × 2⁵² + // r4 < 2¹⁰⁷ + // + + c0 := shiftRightBy51(r0) + c1 := shiftRightBy51(r1) + c2 := shiftRightBy51(r2) + c3 := shiftRightBy51(r3) + c4 := shiftRightBy51(r4) + + rr0 := r0.lo&maskLow51Bits + c4*19 + rr1 := r1.lo&maskLow51Bits + c0 + rr2 := r2.lo&maskLow51Bits + c1 + rr3 := r3.lo&maskLow51Bits + c2 + rr4 := r4.lo&maskLow51Bits + c3 + + // Now all coefficients fit into 64-bit registers but are still too large to + // be passed around as a Element. We therefore do one last carry chain, + // where the carries will be small enough to fit in the wiggle room above 2⁵¹. + *v = Element{rr0, rr1, rr2, rr3, rr4} + v.carryPropagate() +} + +func feSquareGeneric(v, a *Element) { + l0 := a.l0 + l1 := a.l1 + l2 := a.l2 + l3 := a.l3 + l4 := a.l4 + + // Squaring works precisely like multiplication above, but thanks to its + // symmetry we get to group a few terms together. + // + // l4 l3 l2 l1 l0 x + // l4 l3 l2 l1 l0 = + // ------------------------ + // l4l0 l3l0 l2l0 l1l0 l0l0 + + // l4l1 l3l1 l2l1 l1l1 l0l1 + + // l4l2 l3l2 l2l2 l1l2 l0l2 + + // l4l3 l3l3 l2l3 l1l3 l0l3 + + // l4l4 l3l4 l2l4 l1l4 l0l4 = + // ---------------------------------------------- + // r8 r7 r6 r5 r4 r3 r2 r1 r0 + // + // l4l0 l3l0 l2l0 l1l0 l0l0 + + // l3l1 l2l1 l1l1 l0l1 19×l4l1 + + // l2l2 l1l2 l0l2 19×l4l2 19×l3l2 + + // l1l3 l0l3 19×l4l3 19×l3l3 19×l2l3 + + // l0l4 19×l4l4 19×l3l4 19×l2l4 19×l1l4 = + // -------------------------------------- + // r4 r3 r2 r1 r0 + // + // With precomputed 2×, 19×, and 2×19× terms, we can compute each limb with + // only three Mul64 and four Add64, instead of five and eight. + + l0_2 := l0 * 2 + l1_2 := l1 * 2 + + l1_38 := l1 * 38 + l2_38 := l2 * 38 + l3_38 := l3 * 38 + + l3_19 := l3 * 19 + l4_19 := l4 * 19 + + // r0 = l0×l0 + 19×(l1×l4 + l2×l3 + l3×l2 + l4×l1) = l0×l0 + 19×2×(l1×l4 + l2×l3) + r0 := mul64(l0, l0) + r0 = addMul64(r0, l1_38, l4) + r0 = addMul64(r0, l2_38, l3) + + // r1 = l0×l1 + l1×l0 + 19×(l2×l4 + l3×l3 + l4×l2) = 2×l0×l1 + 19×2×l2×l4 + 19×l3×l3 + r1 := mul64(l0_2, l1) + r1 = addMul64(r1, l2_38, l4) + r1 = addMul64(r1, l3_19, l3) + + // r2 = l0×l2 + l1×l1 + l2×l0 + 19×(l3×l4 + l4×l3) = 2×l0×l2 + l1×l1 + 19×2×l3×l4 + r2 := mul64(l0_2, l2) + r2 = addMul64(r2, l1, l1) + r2 = addMul64(r2, l3_38, l4) + + // r3 = l0×l3 + l1×l2 + l2×l1 + l3×l0 + 19×l4×l4 = 2×l0×l3 + 2×l1×l2 + 19×l4×l4 + r3 := mul64(l0_2, l3) + r3 = addMul64(r3, l1_2, l2) + r3 = addMul64(r3, l4_19, l4) + + // r4 = l0×l4 + l1×l3 + l2×l2 + l3×l1 + l4×l0 = 2×l0×l4 + 2×l1×l3 + l2×l2 + r4 := mul64(l0_2, l4) + r4 = addMul64(r4, l1_2, l3) + r4 = addMul64(r4, l2, l2) + + c0 := shiftRightBy51(r0) + c1 := shiftRightBy51(r1) + c2 := shiftRightBy51(r2) + c3 := shiftRightBy51(r3) + c4 := shiftRightBy51(r4) + + rr0 := r0.lo&maskLow51Bits + c4*19 + rr1 := r1.lo&maskLow51Bits + c0 + rr2 := r2.lo&maskLow51Bits + c1 + rr3 := r3.lo&maskLow51Bits + c2 + rr4 := r4.lo&maskLow51Bits + c3 + + *v = Element{rr0, rr1, rr2, rr3, rr4} + v.carryPropagate() +} + +// carryPropagate brings the limbs below 52 bits by applying the reduction +// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry. TODO inline +func (v *Element) carryPropagateGeneric() *Element { + c0 := v.l0 >> 51 + c1 := v.l1 >> 51 + c2 := v.l2 >> 51 + c3 := v.l3 >> 51 + c4 := v.l4 >> 51 + + v.l0 = v.l0&maskLow51Bits + c4*19 + v.l1 = v.l1&maskLow51Bits + c0 + v.l2 = v.l2&maskLow51Bits + c1 + v.l3 = v.l3&maskLow51Bits + c2 + v.l4 = v.l4&maskLow51Bits + c3 + + return v +} diff --git a/openpgp/internal/ecc/curve25519/field/fe_test.go b/openpgp/internal/ecc/curve25519/field/fe_test.go new file mode 100644 index 000000000..b484459ff --- /dev/null +++ b/openpgp/internal/ecc/curve25519/field/fe_test.go @@ -0,0 +1,558 @@ +// Copyright (c) 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package field + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "io" + "math/big" + "math/bits" + mathrand "math/rand" + "reflect" + "testing" + "testing/quick" +) + +func (v Element) String() string { + return hex.EncodeToString(v.Bytes()) +} + +// quickCheckConfig1024 will make each quickcheck test run (1024 * -quickchecks) +// times. The default value of -quickchecks is 100. +var quickCheckConfig1024 = &quick.Config{MaxCountScale: 1 << 10} + +func generateFieldElement(rand *mathrand.Rand) Element { + const maskLow52Bits = (1 << 52) - 1 + return Element{ + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + rand.Uint64() & maskLow52Bits, + } +} + +// weirdLimbs can be combined to generate a range of edge-case field elements. +// 0 and -1 are intentionally more weighted, as they combine well. +var ( + weirdLimbs51 = []uint64{ + 0, 0, 0, 0, + 1, + 19 - 1, + 19, + 0x2aaaaaaaaaaaa, + 0x5555555555555, + (1 << 51) - 20, + (1 << 51) - 19, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + } + weirdLimbs52 = []uint64{ + 0, 0, 0, 0, 0, 0, + 1, + 19 - 1, + 19, + 0x2aaaaaaaaaaaa, + 0x5555555555555, + (1 << 51) - 20, + (1 << 51) - 19, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + (1 << 51) - 1, (1 << 51) - 1, + 1 << 51, + (1 << 51) + 1, + (1 << 52) - 19, + (1 << 52) - 1, + } +) + +func generateWeirdFieldElement(rand *mathrand.Rand) Element { + return Element{ + weirdLimbs52[rand.Intn(len(weirdLimbs52))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + weirdLimbs51[rand.Intn(len(weirdLimbs51))], + } +} + +func (Element) Generate(rand *mathrand.Rand, size int) reflect.Value { + if rand.Intn(2) == 0 { + return reflect.ValueOf(generateWeirdFieldElement(rand)) + } + return reflect.ValueOf(generateFieldElement(rand)) +} + +// isInBounds returns whether the element is within the expected bit size bounds +// after a light reduction. +func isInBounds(x *Element) bool { + return bits.Len64(x.l0) <= 52 && + bits.Len64(x.l1) <= 52 && + bits.Len64(x.l2) <= 52 && + bits.Len64(x.l3) <= 52 && + bits.Len64(x.l4) <= 52 +} + +func TestMultiplyDistributesOverAdd(t *testing.T) { + multiplyDistributesOverAdd := func(x, y, z Element) bool { + // Compute t1 = (x+y)*z + t1 := new(Element) + t1.Add(&x, &y) + t1.Multiply(t1, &z) + + // Compute t2 = x*z + y*z + t2 := new(Element) + t3 := new(Element) + t2.Multiply(&x, &z) + t3.Multiply(&y, &z) + t2.Add(t2, t3) + + return t1.Equal(t2) == 1 && isInBounds(t1) && isInBounds(t2) + } + + if err := quick.Check(multiplyDistributesOverAdd, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestMul64to128(t *testing.T) { + a := uint64(5) + b := uint64(5) + r := mul64(a, b) + if r.lo != 0x19 || r.hi != 0 { + t.Errorf("lo-range wide mult failed, got %d + %d*(2**64)", r.lo, r.hi) + } + + a = uint64(18014398509481983) // 2^54 - 1 + b = uint64(18014398509481983) // 2^54 - 1 + r = mul64(a, b) + if r.lo != 0xff80000000000001 || r.hi != 0xfffffffffff { + t.Errorf("hi-range wide mult failed, got %d + %d*(2**64)", r.lo, r.hi) + } + + a = uint64(1125899906842661) + b = uint64(2097155) + r = mul64(a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + r = addMul64(r, a, b) + if r.lo != 16888498990613035 || r.hi != 640 { + t.Errorf("wrong answer: %d + %d*(2**64)", r.lo, r.hi) + } +} + +func TestSetBytesRoundTrip(t *testing.T) { + f1 := func(in [32]byte, fe Element) bool { + fe.SetBytes(in[:]) + + // Mask the most significant bit as it's ignored by SetBytes. (Now + // instead of earlier so we check the masking in SetBytes is working.) + in[len(in)-1] &= (1 << 7) - 1 + + return bytes.Equal(in[:], fe.Bytes()) && isInBounds(&fe) + } + if err := quick.Check(f1, nil); err != nil { + t.Errorf("failed bytes->FE->bytes round-trip: %v", err) + } + + f2 := func(fe, r Element) bool { + r.SetBytes(fe.Bytes()) + + // Intentionally not using Equal not to go through Bytes again. + // Calling reduce because both Generate and SetBytes can produce + // non-canonical representations. + fe.reduce() + r.reduce() + return fe == r + } + if err := quick.Check(f2, nil); err != nil { + t.Errorf("failed FE->bytes->FE round-trip: %v", err) + } + + // Check some fixed vectors from dalek + type feRTTest struct { + fe Element + b []byte + } + var tests = []feRTTest{ + { + fe: Element{358744748052810, 1691584618240980, 977650209285361, 1429865912637724, 560044844278676}, + b: []byte{74, 209, 69, 197, 70, 70, 161, 222, 56, 226, 229, 19, 112, 60, 25, 92, 187, 74, 222, 56, 50, 153, 51, 233, 40, 74, 57, 6, 160, 185, 213, 31}, + }, + { + fe: Element{84926274344903, 473620666599931, 365590438845504, 1028470286882429, 2146499180330972}, + b: []byte{199, 23, 106, 112, 61, 77, 216, 79, 186, 60, 11, 118, 13, 16, 103, 15, 42, 32, 83, 250, 44, 57, 204, 198, 78, 199, 253, 119, 146, 172, 3, 122}, + }, + } + + for _, tt := range tests { + b := tt.fe.Bytes() + if !bytes.Equal(b, tt.b) || new(Element).SetBytes(tt.b).Equal(&tt.fe) != 1 { + t.Errorf("Failed fixed roundtrip: %v", tt) + } + } +} + +func swapEndianness(buf []byte) []byte { + for i := 0; i < len(buf)/2; i++ { + buf[i], buf[len(buf)-i-1] = buf[len(buf)-i-1], buf[i] + } + return buf +} + +func TestBytesBigEquivalence(t *testing.T) { + f1 := func(in [32]byte, fe, fe1 Element) bool { + fe.SetBytes(in[:]) + + in[len(in)-1] &= (1 << 7) - 1 // mask the most significant bit + b := new(big.Int).SetBytes(swapEndianness(in[:])) + fe1.fromBig(b) + + if fe != fe1 { + return false + } + + buf := make([]byte, 32) // pad with zeroes + copy(buf, swapEndianness(fe1.toBig().Bytes())) + + return bytes.Equal(fe.Bytes(), buf) && isInBounds(&fe) && isInBounds(&fe1) + } + if err := quick.Check(f1, nil); err != nil { + t.Error(err) + } +} + +// fromBig sets v = n, and returns v. The bit length of n must not exceed 256. +func (v *Element) fromBig(n *big.Int) *Element { + if n.BitLen() > 32*8 { + panic("edwards25519: invalid field element input size") + } + + buf := make([]byte, 0, 32) + for _, word := range n.Bits() { + for i := 0; i < bits.UintSize; i += 8 { + if len(buf) >= cap(buf) { + break + } + buf = append(buf, byte(word)) + word >>= 8 + } + } + + return v.SetBytes(buf[:32]) +} + +func (v *Element) fromDecimal(s string) *Element { + n, ok := new(big.Int).SetString(s, 10) + if !ok { + panic("not a valid decimal: " + s) + } + return v.fromBig(n) +} + +// toBig returns v as a big.Int. +func (v *Element) toBig() *big.Int { + buf := v.Bytes() + + words := make([]big.Word, 32*8/bits.UintSize) + for n := range words { + for i := 0; i < bits.UintSize; i += 8 { + if len(buf) == 0 { + break + } + words[n] |= big.Word(buf[0]) << big.Word(i) + buf = buf[1:] + } + } + + return new(big.Int).SetBits(words) +} + +func TestDecimalConstants(t *testing.T) { + sqrtM1String := "19681161376707505956807079304988542015446066515923890162744021073123829784752" + if exp := new(Element).fromDecimal(sqrtM1String); sqrtM1.Equal(exp) != 1 { + t.Errorf("sqrtM1 is %v, expected %v", sqrtM1, exp) + } + // d is in the parent package, and we don't want to expose d or fromDecimal. + // dString := "37095705934669439343138083508754565189542113879843219016388785533085940283555" + // if exp := new(Element).fromDecimal(dString); d.Equal(exp) != 1 { + // t.Errorf("d is %v, expected %v", d, exp) + // } +} + +func TestSetBytesRoundTripEdgeCases(t *testing.T) { + // TODO: values close to 0, close to 2^255-19, between 2^255-19 and 2^255-1, + // and between 2^255 and 2^256-1. Test both the documented SetBytes + // behavior, and that Bytes reduces them. +} + +// Tests self-consistency between Multiply and Square. +func TestConsistency(t *testing.T) { + var x Element + var x2, x2sq Element + + x = Element{1, 1, 1, 1, 1} + x2.Multiply(&x, &x) + x2sq.Square(&x) + + if x2 != x2sq { + t.Fatalf("all ones failed\nmul: %x\nsqr: %x\n", x2, x2sq) + } + + var bytes [32]byte + + _, err := io.ReadFull(rand.Reader, bytes[:]) + if err != nil { + t.Fatal(err) + } + x.SetBytes(bytes[:]) + + x2.Multiply(&x, &x) + x2sq.Square(&x) + + if x2 != x2sq { + t.Fatalf("all ones failed\nmul: %x\nsqr: %x\n", x2, x2sq) + } +} + +func TestEqual(t *testing.T) { + x := Element{1, 1, 1, 1, 1} + y := Element{5, 4, 3, 2, 1} + + eq := x.Equal(&x) + if eq != 1 { + t.Errorf("wrong about equality") + } + + eq = x.Equal(&y) + if eq != 0 { + t.Errorf("wrong about inequality") + } +} + +func TestInvert(t *testing.T) { + x := Element{1, 1, 1, 1, 1} + one := Element{1, 0, 0, 0, 0} + var xinv, r Element + + xinv.Invert(&x) + r.Multiply(&x, &xinv) + r.reduce() + + if one != r { + t.Errorf("inversion identity failed, got: %x", r) + } + + var bytes [32]byte + + _, err := io.ReadFull(rand.Reader, bytes[:]) + if err != nil { + t.Fatal(err) + } + x.SetBytes(bytes[:]) + + xinv.Invert(&x) + r.Multiply(&x, &xinv) + r.reduce() + + if one != r { + t.Errorf("random inversion identity failed, got: %x for field element %x", r, x) + } + + zero := Element{} + x.Set(&zero) + if xx := xinv.Invert(&x); xx != &xinv { + t.Errorf("inverting zero did not return the receiver") + } else if xinv.Equal(&zero) != 1 { + t.Errorf("inverting zero did not return zero") + } +} + +func TestSelectSwap(t *testing.T) { + a := Element{358744748052810, 1691584618240980, 977650209285361, 1429865912637724, 560044844278676} + b := Element{84926274344903, 473620666599931, 365590438845504, 1028470286882429, 2146499180330972} + + var c, d Element + + c.Select(&a, &b, 1) + d.Select(&a, &b, 0) + + if c.Equal(&a) != 1 || d.Equal(&b) != 1 { + t.Errorf("Select failed") + } + + c.Swap(&d, 0) + + if c.Equal(&a) != 1 || d.Equal(&b) != 1 { + t.Errorf("Swap failed") + } + + c.Swap(&d, 1) + + if c.Equal(&b) != 1 || d.Equal(&a) != 1 { + t.Errorf("Swap failed") + } +} + +func TestMult32(t *testing.T) { + mult32EquivalentToMul := func(x Element, y uint32) bool { + t1 := new(Element) + for i := 0; i < 100; i++ { + t1.Mult32(&x, y) + } + + ty := new(Element) + ty.l0 = uint64(y) + + t2 := new(Element) + for i := 0; i < 100; i++ { + t2.Multiply(&x, ty) + } + + return t1.Equal(t2) == 1 && isInBounds(t1) && isInBounds(t2) + } + + if err := quick.Check(mult32EquivalentToMul, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestSqrtRatio(t *testing.T) { + // From draft-irtf-cfrg-ristretto255-decaf448-00, Appendix A.4. + type test struct { + u, v string + wasSquare int + r string + } + var tests = []test{ + // If u is 0, the function is defined to return (0, TRUE), even if v + // is zero. Note that where used in this package, the denominator v + // is never zero. + { + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + 1, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // 0/1 == 0² + { + "0000000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 1, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // If u is non-zero and v is zero, defined to return (0, FALSE). + { + "0100000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + 0, "0000000000000000000000000000000000000000000000000000000000000000", + }, + // 2/1 is not square in this field. + { + "0200000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 0, "3c5ff1b5d8e4113b871bd052f9e7bcd0582804c266ffb2d4f4203eb07fdb7c54", + }, + // 4/1 == 2² + { + "0400000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + 1, "0200000000000000000000000000000000000000000000000000000000000000", + }, + // 1/4 == (2⁻¹)² == (2^(p-2))² per Euler's theorem + { + "0100000000000000000000000000000000000000000000000000000000000000", + "0400000000000000000000000000000000000000000000000000000000000000", + 1, "f6ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3f", + }, + } + + for i, tt := range tests { + u := new(Element).SetBytes(decodeHex(tt.u)) + v := new(Element).SetBytes(decodeHex(tt.v)) + want := new(Element).SetBytes(decodeHex(tt.r)) + got, wasSquare := new(Element).SqrtRatio(u, v) + if got.Equal(want) == 0 || wasSquare != tt.wasSquare { + t.Errorf("%d: got (%v, %v), want (%v, %v)", i, got, wasSquare, want, tt.wasSquare) + } + } +} + +func TestCarryPropagate(t *testing.T) { + asmLikeGeneric := func(a [5]uint64) bool { + t1 := &Element{a[0], a[1], a[2], a[3], a[4]} + t2 := &Element{a[0], a[1], a[2], a[3], a[4]} + + t1.carryPropagate() + t2.carryPropagateGeneric() + + if *t1 != *t2 { + t.Logf("got: %#v,\nexpected: %#v", t1, t2) + } + + return *t1 == *t2 && isInBounds(t2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } + + if !asmLikeGeneric([5]uint64{0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff}) { + t.Errorf("failed for {0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff}") + } +} + +func TestFeSquare(t *testing.T) { + asmLikeGeneric := func(a Element) bool { + t1 := a + t2 := a + + feSquareGeneric(&t1, &t1) + feSquare(&t2, &t2) + + if t1 != t2 { + t.Logf("got: %#v,\nexpected: %#v", t1, t2) + } + + return t1 == t2 && isInBounds(&t2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func TestFeMul(t *testing.T) { + asmLikeGeneric := func(a, b Element) bool { + a1 := a + a2 := a + b1 := b + b2 := b + + feMulGeneric(&a1, &a1, &b1) + feMul(&a2, &a2, &b2) + + if a1 != a2 || b1 != b2 { + t.Logf("got: %#v,\nexpected: %#v", a1, a2) + t.Logf("got: %#v,\nexpected: %#v", b1, b2) + } + + return a1 == a2 && isInBounds(&a2) && + b1 == b2 && isInBounds(&b2) + } + + if err := quick.Check(asmLikeGeneric, quickCheckConfig1024); err != nil { + t.Error(err) + } +} + +func decodeHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} diff --git a/openpgp/internal/ecc/curves.go b/openpgp/internal/ecc/curves.go index 5ed9c93b3..34c4ad860 100644 --- a/openpgp/internal/ecc/curves.go +++ b/openpgp/internal/ecc/curves.go @@ -16,6 +16,8 @@ type ECDSACurve interface { UnmarshalIntegerPoint([]byte) (x, y *big.Int) MarshalIntegerSecret(d *big.Int) []byte UnmarshalIntegerSecret(d []byte) *big.Int + MarshalFieldInteger(d *big.Int) []byte + UnmarshalFieldInteger(d []byte) *big.Int GenerateECDSA(rand io.Reader) (x, y, secret *big.Int, err error) Sign(rand io.Reader, x, y, d *big.Int, hash []byte) (r, s *big.Int, err error) Verify(x, y *big.Int, hash []byte, r, s *big.Int) bool diff --git a/openpgp/internal/ecc/generic.go b/openpgp/internal/ecc/generic.go index e28d7c710..44fad3b49 100644 --- a/openpgp/internal/ecc/generic.go +++ b/openpgp/internal/ecc/generic.go @@ -56,6 +56,15 @@ func (c *genericCurve) UnmarshalIntegerSecret(d []byte) *big.Int { return new(big.Int).SetBytes(d) } +func (c *genericCurve) MarshalFieldInteger(i *big.Int) (b []byte) { + b = make([]byte, (c.Curve.Params().BitSize+7)/8) + return i.FillBytes(b) +} + +func (c *genericCurve) UnmarshalFieldInteger(d []byte) *big.Int { + return new(big.Int).SetBytes(d) +} + func (c *genericCurve) GenerateECDH(rand io.Reader) (point, secret []byte, err error) { secret, x, y, err := elliptic.GenerateKey(c.Curve, rand) if err != nil { diff --git a/openpgp/internal/encoding/octetarray.go b/openpgp/internal/encoding/octetarray.go new file mode 100644 index 000000000..e5e4a8274 --- /dev/null +++ b/openpgp/internal/encoding/octetarray.go @@ -0,0 +1,65 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package encoding + +import ( + "io" +) + +// OctetArray is used to store a fixed-length field +type OctetArray struct { + length int + bytes []byte +} + +// NewOctetArray returns a OID initialized with bytes. +func NewOctetArray(bytes []byte) *OctetArray { + return &OctetArray{ + length: len(bytes), + bytes: bytes, + } +} + +func NewEmptyOctetArray(length int) *OctetArray { + return &OctetArray{ + length: length, + bytes: nil, + } +} + +// Bytes returns the decoded data. +func (o *OctetArray) Bytes() []byte { + return o.bytes +} + +// BitLength is the size in bits of the decoded data. +func (o *OctetArray) BitLength() uint16 { + return uint16(o.length * 8) +} + +// EncodedBytes returns the encoded data. +func (o *OctetArray) EncodedBytes() []byte { + if len(o.bytes) != o.length { + panic("invalid length") + } + return o.bytes +} + +// EncodedLength is the size in bytes of the encoded data. +func (o *OctetArray) EncodedLength() uint16 { + return uint16(o.length) +} + +// ReadFrom reads into b the next OID from r. +func (o *OctetArray) ReadFrom(r io.Reader) (int64, error) { + o.bytes = make([]byte, o.length) + + nn, err := io.ReadFull(r, o.bytes) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + return int64(nn), err +} diff --git a/openpgp/internal/encoding/short_byte_string.go b/openpgp/internal/encoding/short_byte_string.go new file mode 100644 index 000000000..0c3b91233 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string.go @@ -0,0 +1,50 @@ +package encoding + +import ( + "io" +) + +type ShortByteString struct { + length uint8 + data []byte +} + +func NewShortByteString(data []byte) *ShortByteString { + byteLength := uint8(len(data)) + + return &ShortByteString{byteLength, data} +} + +func (byteString *ShortByteString) Bytes() []byte { + return byteString.data +} + +func (byteString *ShortByteString) BitLength() uint16 { + return uint16(byteString.length) * 8 +} + +func (byteString *ShortByteString) EncodedBytes() []byte { + encodedLength := [1]byte{ + uint8(byteString.length), + } + return append(encodedLength[:], byteString.data...) +} + +func (byteString *ShortByteString) EncodedLength() uint16 { + return uint16(byteString.length) + 1 +} + +func (byteString *ShortByteString) ReadFrom(r io.Reader) (int64, error) { + var lengthBytes [1]byte + if n, err := io.ReadFull(r, lengthBytes[:]); err != nil { + return int64(n), err + } + + byteString.length = uint8(lengthBytes[0]) + + byteString.data = make([]byte, byteString.length) + if n, err := io.ReadFull(r, byteString.data); err != nil { + return int64(n + 1), err + } + return int64(byteString.length + 1), nil +} diff --git a/openpgp/internal/encoding/short_byte_string_test.go b/openpgp/internal/encoding/short_byte_string_test.go new file mode 100644 index 000000000..37510a355 --- /dev/null +++ b/openpgp/internal/encoding/short_byte_string_test.go @@ -0,0 +1,61 @@ +package encoding + +import ( + "bytes" + "testing" +) + +var octetStreamTests = []struct { + data []byte +}{ + { + data: []byte{0x0, 0x0, 0x0}, + }, + { + data: []byte{0x1, 0x2, 0x03}, + }, + { + data: make([]byte, 255), + }, +} + +func TestShortByteString(t *testing.T) { + for i, test := range octetStreamTests { + octetStream := NewShortByteString(test.data) + + if b := octetStream.Bytes(); !bytes.Equal(b, test.data) { + t.Errorf("#%d: bad creation got:%x want:%x", i, b, test.data) + } + + expectedBitLength := uint16(len(test.data)) * 8 + if bitLength := octetStream.BitLength(); bitLength != expectedBitLength { + t.Errorf("#%d: bad bit length got:%d want :%d", i, bitLength, expectedBitLength) + } + + expectedEncodedLength := uint16(len(test.data)) + 1 + if encodedLength := octetStream.EncodedLength(); encodedLength != expectedEncodedLength { + t.Errorf("#%d: bad encoded length got:%d want:%d", i, encodedLength, expectedEncodedLength) + } + + encodedBytes := octetStream.EncodedBytes() + if !bytes.Equal(encodedBytes[1:], test.data) { + t.Errorf("#%d: bad encoded bytes got:%x want:%x", i, encodedBytes[1:], test.data) + } + + encodedLength := int(encodedBytes[0]) + if encodedLength != len(test.data) { + t.Errorf("#%d: bad encoded length got:%d want%d", i, encodedLength, len(test.data)) + } + + newStream := new(ShortByteString) + newStream.ReadFrom(bytes.NewReader(encodedBytes)) + + if !checkEquality(newStream, octetStream) { + t.Errorf("#%d: bad parsing of encoded octet stream", i) + } + } +} + +func checkEquality(left *ShortByteString, right *ShortByteString) bool { + return (left.length == right.length) && (bytes.Equal(left.data, right.data)) +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 77213f66b..1b1387f06 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -21,7 +21,11 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -319,6 +323,35 @@ 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) + case packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSH key") + } + + scheme, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), scheme) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -326,7 +359,8 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // Generates an encryption/decryption key func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { - switch config.PublicKeyAlgorithm() { + pubKeyAlgo := config.PublicKeyAlgorithm() + switch pubKeyAlgo { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() if bits < 1024 { @@ -361,6 +395,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, packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + 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 a071353e2..34bffb97e 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 3cb4ac005..8bddbeb7a 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 fc9ba776d..7914d3eb0 100644 --- a/openpgp/keys_v6_test.go +++ b/openpgp/keys_v6_test.go @@ -3,8 +3,14 @@ package openpgp import ( "bytes" "crypto" + "crypto/rand" "strings" "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -196,3 +202,138 @@ func TestNewEntityWithDefaultHashv6(t *testing.T) { } } } + +func TestGeneratePqKey(t *testing.T) { + randomPassword := make([]byte, 128) + _, err := rand.Read(randomPassword) + if err != nil { + t.Fatal(err) + } + + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA65_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA87_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for name, algo := range asymmAlgos { + t.Run(name, func(t *testing.T) { + config := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: algo, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", config) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + err = entity.SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + read, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatalf("Failed to parse entity: %s", err) + } + + if read.PrimaryKey.PubKeyAlgo != algo { + t.Fatalf("Expected subkey algorithm: %v, got: %v", algo, read.PrimaryKey.PubKeyAlgo) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err := read.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-DSA key was marked as invalid: ", err) + } + + if err = read.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if pk, ok := read.PrivateKey.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey); ok { + bin, err := pk.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + if pk.PublicMldsa, err = pk.Mldsa.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal(err) + } + } + + err = read.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-DSA key") + } + + testMlkemSubkey(t, read.Subkeys[0], randomPassword) + }) + } +} + +func testMlkemSubkey(t *testing.T, subkey Subkey, randomPassword []byte) { + var err error + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + if err = subkey.PrivateKey.Decrypt(randomPassword); err != nil { + t.Fatal("Valid ML-KEM key was marked as invalid: ", err) + } + + if err = subkey.PrivateKey.Encrypt(randomPassword); err != nil { + t.Fatal(err) + } + + // Corrupt public ML-KEM in primary key + if pk, ok := subkey.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey); ok { + bin, _ := pk.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + if pk.PublicMlkem, err = pk.Mlkem.UnmarshalBinaryPublicKey(bin); err != nil { + t.Fatal("unable to corrupt key") + } + } else { + t.Fatal("Invalid subkey") + } + + err = subkey.PrivateKey.Decrypt(randomPassword) + if _, ok := err.(errors.KeyInvalidError); !ok { + t.Fatal("Failed to detect invalid ML-KEM key") + } +} + +func TestAddV6MlkemSubkey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + testAddMlkemSubkey(t, entity, true) +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa.go b/openpgp/mldsa_eddsa/mldsa_eddsa.go new file mode 100644 index 000000000..82cdf199c --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa.go @@ -0,0 +1,105 @@ +// Package mldsa_eddsa implements hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-composite-signature-schemes +package mldsa_eddsa + +import ( + goerrors "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/sign" +) + +const ( + MlDsaSeedLen = 32 +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.EdDSACurve + Mldsa sign.Scheme + PublicPoint []byte + PublicMldsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMldsa sign.PrivateKey + SecretMldsaSeed []byte +} + +// GenerateKey generates a ML-DSA + EdDSA composite key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-generation-procedure-2 +func GenerateKey(rand io.Reader, algId uint8, c ecc.EdDSACurve, d sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mldsa = d + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateEdDSA(rand) + if err != nil { + return nil, err + } + + keySeed := make([]byte, d.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + + if err := priv.DeriveMlDsaKeys(keySeed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlDsaKeys derives the ML-DSA keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlDsaKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlDsaSeedLen { + return goerrors.New("mldsa_eddsa: ml-dsa secret seed has the wrong length") + } + priv.SecretMldsaSeed = seed + publicKey, privateKey := priv.PublicKey.Mldsa.DeriveKey(priv.SecretMldsaSeed) + if overridePublicKey { + priv.PublicKey.PublicMldsa = publicKey + } + priv.SecretMldsa = privateKey + return nil +} + +// Sign generates a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-generation +func Sign(priv *PrivateKey, message []byte) (dSig, ecSig []byte, err error) { + ecSig, err = priv.PublicKey.Curve.Sign(priv.PublicKey.PublicPoint, priv.SecretEc, message) + if err != nil { + return nil, nil, err + } + + dSig = priv.PublicKey.Mldsa.Sign(priv.SecretMldsa, message, nil) + if dSig == nil { + return nil, nil, goerrors.New("mldsa_eddsa: unable to sign with ML-DSA") + } + + return dSig, ecSig, nil +} + +// Verify verifies a ML-DSA + EdDSA composite signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-verification +func Verify(pub *PublicKey, message, dSig, ecSig []byte) bool { + return pub.Curve.Verify(pub.PublicPoint, message, ecSig) && pub.Mldsa.Verify(pub.PublicMldsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateEdDSA(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicMldsa.Equal(priv.SecretMldsa.Public()) { + return errors.KeyInvalidError("mldsa_eddsa: invalid public key") + } + + return nil +} diff --git a/openpgp/mldsa_eddsa/mldsa_eddsa_test.go b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go new file mode 100644 index 000000000..20b9e1604 --- /dev/null +++ b/openpgp/mldsa_eddsa/mldsa_eddsa_test.go @@ -0,0 +1,95 @@ +// Package mldsa_eddsa_test tests the implementation of hybrid ML-DSA + EdDSA encryption, suitable for OpenPGP, experimental. +package mldsa_eddsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestSignVerify(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "ML-DSA3_Ed25519": packet.PubKeyAlgoMldsa65Ed25519, + "ML-DSA5_Ed448": packet.PubKeyAlgoMldsa87Ed448, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicMldsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicMldsa, err = key.Mldsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mldsa_eddsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mldsa_eddsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mldsa_eddsa.PrivateKey { + curveObj, err := packet.GetEdDSACurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMldsaFromAlgID(algId) + if err != nil { + t.Errorf("error getting ML-DSA: %s", err) + } + + priv, err := mldsa_eddsa.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *mldsa_eddsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, ecSig, err := mldsa_eddsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := mldsa_eddsa.Verify(&priv.PublicKey, digest, dSig, ecSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh.go b/openpgp/mlkem_ecdh/mlkem_ecdh.go new file mode 100644 index 000000000..d509c05ba --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh.go @@ -0,0 +1,273 @@ +// Package mlkem_ecdh implements hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +// It follows the spec https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-composite-kem-schemes +package mlkem_ecdh + +import ( + "encoding/hex" + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" + "golang.org/x/crypto/sha3" + + "github.com/ProtonMail/go-crypto/openpgp/aes/keywrap" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/cloudflare/circl/kem" +) + +const ( + maxSessionKeyLength = 64 + MlKemSeedLen = 64 + kdfContext = "OpenPGPCompositeKDFv1" +) + +type PublicKey struct { + AlgId uint8 + Curve ecc.ECDHCurve + Mlkem kem.Scheme + PublicMlkem kem.PublicKey + PublicPoint []byte +} + +type PrivateKey struct { + PublicKey + SecretEc []byte + SecretMlkem kem.PrivateKey + SecretMlkemSeed []byte +} + +// GenerateKey implements ML-KEM + ECC key generation as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-generation-procedure +func GenerateKey(rand io.Reader, algId uint8, c ecc.ECDHCurve, k kem.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Curve = c + priv.PublicKey.Mlkem = k + + priv.PublicKey.PublicPoint, priv.SecretEc, err = c.GenerateECDH(rand) + if err != nil { + return nil, err + } + + seed, err := generateRandomSeed(rand, MlKemSeedLen) + if err != nil { + return nil, err + } + + if err := priv.DeriveMlKemKeys(seed, true); err != nil { + return nil, err + } + return priv, nil +} + +// DeriveMlKemKeys derives the ML-KEM keys from the provided seed and stores them inside priv. +func (priv *PrivateKey) DeriveMlKemKeys(seed []byte, overridePublicKey bool) (err error) { + if len(seed) != MlKemSeedLen { + return goerrors.New("mlkem_ecdh: ml-kem secret seed has the wrong length") + } + priv.SecretMlkemSeed = seed + publicKey, privateKey := priv.PublicKey.Mlkem.DeriveKeyPair(priv.SecretMlkemSeed) + if overridePublicKey { + priv.PublicKey.PublicMlkem = publicKey + } + priv.SecretMlkem = privateKey + return nil +} + +// Encrypt implements ML-KEM + ECC encryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-encryption-procedure +func Encrypt(rand io.Reader, pub *PublicKey, msg []byte) (kEphemeral, ecEphemeral, ciphertext []byte, err error) { + if len(msg) > maxSessionKeyLength { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key too long") + } + + if len(msg)%8 != 0 { + return nil, nil, nil, goerrors.New("mlkem_ecdh: session key not a multiple of 8") + } + + // EC shared secret derivation + ecEphemeral, ecSS, err := pub.Curve.Encaps(rand, pub.PublicPoint) + if err != nil { + return nil, nil, nil, err + } + + // ML-KEM shared secret derivation + kyberSeed, err := generateRandomSeed(rand, pub.Mlkem.EncapsulationSeedSize()) + if err != nil { + return nil, nil, nil, err + } + + kEphemeral, kSS, err := pub.Mlkem.EncapsulateDeterministically(pub.PublicMlkem, kyberSeed) + if err != nil { + return nil, nil, nil, err + } + + keyEncryptionKey, err := buildKey(pub, ecSS, ecEphemeral, pub.PublicPoint, kSS, kEphemeral, pub.PublicMlkem) + if err != nil { + return nil, nil, nil, err + } + fmt.Printf("sessionKey: %x\n", msg) + fmt.Printf("keyEncryptionKey: %x\n\n", keyEncryptionKey) + + if ciphertext, err = keywrap.Wrap(keyEncryptionKey, msg); err != nil { + return nil, nil, nil, err + } + + return kEphemeral, ecEphemeral, ciphertext, nil +} + +// Decrypt implements ML-KEM + ECC decryption as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-decryption-procedure +func Decrypt(priv *PrivateKey, kEphemeral, ecEphemeral, ciphertext []byte) (msg []byte, err error) { + // EC shared secret derivation + ecSS, err := priv.PublicKey.Curve.Decaps(ecEphemeral, priv.SecretEc) + if err != nil { + return nil, err + } + + // ML-KEM shared secret derivation + kSS, err := priv.PublicKey.Mlkem.Decapsulate(priv.SecretMlkem, kEphemeral) + if err != nil { + return nil, err + } + + kek, err := buildKey(&priv.PublicKey, ecSS, ecEphemeral, priv.PublicPoint, kSS, kEphemeral, priv.PublicMlkem) + if err != nil { + return nil, err + } + + return keywrap.Unwrap(kek, ciphertext) +} + +// buildKey implements the composite KDF from +// https://github.com/openpgp-pqc/draft-openpgp-pqc/pull/161 +func buildKey(pub *PublicKey, eccSecretPoint, eccEphemeral, eccPublicKey, mlkemKeyShare, mlkemEphemeral []byte, mlkemPublicKey kem.PublicKey) ([]byte, error) { + /// Set the output `ecdhKeyShare` to `eccSecretPoint` + eccKeyShare := eccSecretPoint + + serializedMlkemPublicKey, err := mlkemPublicKey.MarshalBinary() + if err != nil { + return nil, err + } + + // mlkemKeyShare - the ML-KEM key share encoded as an octet string + // mlkemEphemeral - the ML-KEM ciphertext encoded as an octet string + // mlkemPublicKey - The ML-KEM public key of the recipient as an octet string + // algId - the OpenPGP algorithm ID of the public-key encryption algorithm + // 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 + + fmt.Printf("ecdh key share: %s\n", hex.EncodeToString(eccKeyShare)) + fmt.Printf("ml-kem key share: %s\n", hex.EncodeToString(mlkemKeyShare)) + + // SHA3-256(mlkemKeyShare || eccKeyShare || eccEphemeral || eccPublicKey || + // mlkemEphemeral || mlkemPublicKey || algId || "OpenPGPCompositeKDFv1") + h := sha3.New256() + _, _ = h.Write(mlkemKeyShare) + _, _ = h.Write(eccKeyShare) + _, _ = h.Write(eccEphemeral) + _, _ = h.Write(eccPublicKey) + _, _ = h.Write(mlkemEphemeral) + _, _ = h.Write(serializedMlkemPublicKey) + _, _ = h.Write([]byte{pub.AlgId}) + _, _ = h.Write([]byte(kdfContext)) + return h.Sum(nil), nil +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if err = priv.PublicKey.Curve.ValidateECDH(priv.PublicKey.PublicPoint, priv.SecretEc); err != nil { + return err + } + + if !priv.PublicKey.PublicMlkem.Equal(priv.SecretMlkem.Public()) { + return errors.KeyInvalidError("mlkem_ecdh: invalid public key") + } + + return +} + +// EncodeFields encodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey +// and writes it to writer. +func EncodeFields(w io.Writer, ec, ml, encryptedSessionKey []byte, cipherFunction byte, v6 bool) (err error) { + if _, err = w.Write(ec); err != nil { + return err + } + + if _, err = w.Write(ml); err != nil { + return err + } + + lenAlgorithm := 0 + if !v6 { + lenAlgorithm = 1 + } + + if _, err = w.Write([]byte{byte(len(encryptedSessionKey) + lenAlgorithm)}); err != nil { + return err + } + + if !v6 { + if _, err = w.Write([]byte{cipherFunction}); err != nil { + return err + } + } + + if _, err = w.Write(encryptedSessionKey); err != nil { + return err + } + + return nil +} + +// DecodeFields decodes an ML-KEM + ECDH session key encryption fields as +// ephemeral ECDH public key | ML-KEM ciphertext | follow byte length | cipherFunction (v3 only) | encryptedSessionKey. +func DecodeFields(r io.Reader, lenEcc, lenMlkem int, v6 bool) (encryptedMPI1, encryptedMPI2, encryptedMPI3 encoding.Field, cipherFunction byte, err error) { + var buf [1]byte + + encryptedMPI1 = encoding.NewEmptyOctetArray(lenEcc) + if _, err = encryptedMPI1.ReadFrom(r); err != nil { + return + } + + encryptedMPI2 = encoding.NewEmptyOctetArray(lenMlkem) + if _, err = encryptedMPI2.ReadFrom(r); err != nil { + return + } + + // A one-octet size of the following fields. + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + + followingLen := buf[0] + // The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + if !v6 { + if _, err = io.ReadFull(r, buf[:]); err != nil { + return + } + cipherFunction = buf[0] + followingLen -= 1 + } + + // The encrypted session key. + encryptedMPI3 = encoding.NewEmptyOctetArray(int(followingLen)) + if _, err = encryptedMPI3.ReadFrom(r); err != nil { + return + } + + return +} + +func generateRandomSeed(rand io.Reader, size int) ([]byte, error) { + randomBytes := make([]byte, size) + if _, err := rand.Read(randomBytes); err != nil { + return nil, fmt.Errorf("failed to generate random bytes: %w", err) + } + return randomBytes, nil +} diff --git a/openpgp/mlkem_ecdh/mlkem_ecdh_test.go b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go new file mode 100644 index 000000000..23a82eb36 --- /dev/null +++ b/openpgp/mlkem_ecdh/mlkem_ecdh_test.go @@ -0,0 +1,104 @@ +// Package mlkem_ecdh_test tests the implementation of hybrid ML-KEM + ECDH encryption, suitable for OpenPGP, experimental. +package mlkem_ecdh_test + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestEncryptDecrypt(t *testing.T) { + asymmAlgos := map[string]packet.PublicKeyAlgorithm{ + "Mlkem768_X25519": packet.PubKeyAlgoMlkem768X25519, + "Mlkem1024_X448": packet.PubKeyAlgoMlkem1024X448, + } + + symmAlgos := map[string]algorithm.Cipher{ + "AES-128": algorithm.AES128, + "AES-192": algorithm.AES192, + "AES-256": algorithm.AES256, + } + + for asymmName, asymmAlgo := range asymmAlgos { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + for symmName, symmAlgo := range symmAlgos { + t.Run(symmName, func(t *testing.T) { + testEncryptDecryptAlgo(t, key, symmAlgo) + }) + } + testvalidateAlgo(t, asymmAlgo) + }) + } +} + +func testvalidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + var err error + key := testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, _ := key.PublicMlkem.MarshalBinary() + bin[5] ^= 1 + key.PublicMlkem, err = key.Mlkem.UnmarshalBinaryPublicKey(bin) + if err != nil { + t.Fatal("unable to corrupt key") + } + + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } + + // Generate fresh key + key = testGenerateKeyAlgo(t, algId) + if err := mlkem_ecdh.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + key.PublicPoint[5] ^= 1 + if err := mlkem_ecdh.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *mlkem_ecdh.PrivateKey { + curveObj, err := packet.GetECDHCurveFromAlgID(algId) + if err != nil { + t.Errorf("error getting curve: %s", err) + } + + kyberObj, err := packet.GetMlkemFromAlgID(algId) + if err != nil { + t.Errorf("error getting kyber: %s", err) + } + + priv, err := mlkem_ecdh.GenerateKey(rand.Reader, uint8(algId), curveObj, kyberObj) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testEncryptDecryptAlgo(t *testing.T, priv *mlkem_ecdh.PrivateKey, kdfCipher algorithm.Cipher) { + expectedMessage := make([]byte, kdfCipher.KeySize()) // encryption algo + checksum + rand.Read(expectedMessage) + + kE, ecE, c, err := mlkem_ecdh.Encrypt(rand.Reader, &priv.PublicKey, expectedMessage) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + decryptedMessage, err := mlkem_ecdh.Decrypt(priv, kE, ecE, c) + if err != nil { + t.Errorf("error decrypting: %s", err) + } + if !bytes.Equal(decryptedMessage, expectedMessage) { + t.Errorf("decryption failed, got: %x, want: %x", decryptedMessage, expectedMessage) + } +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index b90bb2891..59f1c847f 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") } @@ -346,13 +394,13 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph if version == 6 && pub.PubKeyAlgo == PubKeyAlgoElGamal { return errors.InvalidArgumentError("ElGamal v6 PKESK are not allowed") } - // In v3 PKESKs, for x25519 and x448, mandate using AES - if version == 3 && (pub.PubKeyAlgo == PubKeyAlgoX25519 || pub.PubKeyAlgo == PubKeyAlgoX448) { - switch cipherFunc { - case CipherAES128, CipherAES192, CipherAES256: - break + // In v3 PKESKs, for X25519 and X448, mandate using AES + if version == 3 && cipherFunc != CipherAES128 && cipherFunc != CipherAES192 && cipherFunc != CipherAES256 { + switch pub.PubKeyAlgo { + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: + return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519, x448, and PQC") default: - return errors.InvalidArgumentError("v3 PKESK mandates AES for x25519 and x448") + break } } @@ -389,7 +437,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 @@ -401,7 +449,7 @@ func SerializeEncryptedKeyAEADwithHiddenOption(w io.Writer, pub *PublicKey, ciph keyOffset = 1 } encodeChecksumKey(keyBlock[keyOffset:], key) - case PubKeyAlgoX25519, PubKeyAlgoX448: + case PubKeyAlgoX25519, PubKeyAlgoX448, PubKeyAlgoMlkem768X25519, PubKeyAlgoMlkem1024X448: // algorithm is added in plaintext below keyBlock = key } @@ -417,7 +465,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))) } @@ -442,6 +494,36 @@ func SerializeEncryptedKeyWithHiddenOption(w io.Writer, pub *PublicKey, cipherFu return SerializeEncryptedKeyAEADwithHiddenOption(w, pub, cipherFunc, config.AEAD() != nil, key, hidden, config) } +func (e *EncryptedKey) ProxyTransform(instance ForwardingInstance) (transformed *EncryptedKey, err error) { + if e.Algo != PubKeyAlgoECDH { + return nil, errors.InvalidArgumentError("invalid PKESK") + } + + if e.KeyId != 0 && e.KeyId != instance.GetForwarderKeyId() { + return nil, errors.InvalidArgumentError("invalid key id in PKESK") + } + + ephemeral := e.encryptedMPI1.Bytes() + transformedEphemeral, err := ecdh.ProxyTransform(ephemeral, instance.ProxyParameter) + if err != nil { + return nil, err + } + + wrappedKey := e.encryptedMPI2.Bytes() + copiedWrappedKey := make([]byte, len(wrappedKey)) + copy(copiedWrappedKey, wrappedKey) + + transformed = &EncryptedKey{ + Version: e.Version, + KeyId: instance.getForwardeeKeyIdOrZero(e.KeyId), + Algo: e.Algo, + encryptedMPI1: encoding.NewMPI(transformedEphemeral), + encryptedMPI2: encoding.NewOID(copiedWrappedKey), + } + + return transformed, nil +} + func serializeEncryptedKeyRSA(w io.Writer, rand io.Reader, header []byte, pub *rsa.PublicKey, keyBlock []byte) error { cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) if err != nil { @@ -558,6 +640,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 { @@ -582,3 +693,32 @@ func encodeChecksumKey(buffer []byte, key []byte) { buffer[len(key)] = byte(checksum >> 8) buffer[len(key)+1] = byte(checksum) } + +func serializeEncryptedKeyMlkem(w io.Writer, rand io.Reader, header []byte, pub *mlkem_ecdh.PublicKey, keyBlock []byte, cipherFunc byte, version int) error { + mlE, ecE, c, err := mlkem_ecdh.Encrypt(rand, pub, keyBlock) + if err != nil { + return errors.InvalidArgumentError("ML-KEM + ECDH encryption failed: " + err.Error()) + } + + ml := encoding.NewOctetArray(mlE) + ec := encoding.NewOctetArray(ecE) + m := encoding.NewOctetArray(c) + + packetLen := len(header) /* header length */ + packetLen += int(ec.EncodedLength()) + int(ml.EncodedLength()) + int(m.EncodedLength()) + 1 + if version < 6 { + packetLen += 1 + } + + err = serializeHeader(w, packetTypeEncryptedKey, packetLen) + if err != nil { + return err + } + + _, err = w.Write(header) + if err != nil { + return err + } + + return mlkem_ecdh.EncodeFields(w, ec.EncodedBytes(), ml.EncodedBytes(), m.EncodedBytes(), cipherFunc, version == 6) +} diff --git a/openpgp/packet/encrypted_key_test.go b/openpgp/packet/encrypted_key_test.go index 787c7feca..5ed0a8ed3 100644 --- a/openpgp/packet/encrypted_key_test.go +++ b/openpgp/packet/encrypted_key_test.go @@ -16,6 +16,7 @@ import ( "crypto" "crypto/rsa" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -338,3 +339,33 @@ func TestSerializingEncryptedKey(t *testing.T) { t.Fatalf("serialization of encrypted key differed from original. Original was %s, but reserialized as %s", encryptedKeyHex, bufHex) } } + +func TestSymmetricallyEncryptedKey(t *testing.T) { + const encryptedKeyHex = "c14f03999bd17d726446da64018cb4d628ae753c646b81f87f21269cd733df9db940896a0b0e48f4d3b26e2dfbcf59ca7d30b65ea95ebb072e643407c732c479093b9d180c2eb51c98814e1bbbc6d0a17f" + + expectedNonce := []byte{0x8c, 0xb4, 0xd6, 0x28, 0xae, 0x75, 0x3c, 0x64, 0x6b, 0x81, 0xf8, 0x7f, 0x21, 0x26, 0x9c, 0xd7} + + expectedCiphertext := []byte{0xdf, 0x9d, 0xb9, 0x40, 0x89, 0x6a, 0x0b, 0x0e, 0x48, 0xf4, 0xd3, 0xb2, 0x6e, 0x2d, 0xfb, 0xcf, 0x59, 0xca, 0x7d, 0x30, 0xb6, 0x5e, 0xa9, 0x5e, 0xbb, 0x07, 0x2e, 0x64, 0x34, 0x07, 0xc7, 0x32, 0xc4, 0x79, 0x09, 0x3b, 0x9d, 0x18, 0x0c, 0x2e, 0xb5, 0x1c, 0x98, 0x81, 0x4e, 0x1b, 0xbb, 0xc6, 0xd0, 0xa1, 0x7f} + + p, err := Read(readerFromHex(encryptedKeyHex)) + if err != nil { + t.Fatal("error reading packet") + } + + ek, ok := p.(*EncryptedKey) + if !ok { + t.Fatalf("didn't parse and EncryptedKey, got %#v", p) + } + + if ek.aeadMode != algorithm.AEADModeEAX { + t.Errorf("Parsed wrong aead mode, got %d, expected: 1", ek.aeadMode) + } + + if !bytes.Equal(expectedNonce, ek.nonce) { + t.Errorf("Parsed wrong nonce, got %x, expected %x", ek.nonce, expectedNonce) + } + + if !bytes.Equal(expectedCiphertext, ek.encryptedMPI1.Bytes()) { + t.Errorf("Parsed wrong ciphertext, got %x, expected %x", ek.encryptedMPI1.Bytes(), expectedCiphertext) + } +} diff --git a/openpgp/packet/forwarding.go b/openpgp/packet/forwarding.go new file mode 100644 index 000000000..f16a2fbdc --- /dev/null +++ b/openpgp/packet/forwarding.go @@ -0,0 +1,36 @@ +package packet + +import "encoding/binary" + +// ForwardingInstance represents a single forwarding instance (mapping IDs to a Proxy Param) +type ForwardingInstance struct { + KeyVersion int + ForwarderFingerprint []byte + ForwardeeFingerprint []byte + ProxyParameter []byte +} + +func (f *ForwardingInstance) GetForwarderKeyId() uint64 { + return computeForwardingKeyId(f.ForwarderFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) GetForwardeeKeyId() uint64 { + return computeForwardingKeyId(f.ForwardeeFingerprint, f.KeyVersion) +} + +func (f *ForwardingInstance) getForwardeeKeyIdOrZero(originalKeyId uint64) uint64 { + if originalKeyId == 0 { + return 0 + } + + return f.GetForwardeeKeyId() +} + +func computeForwardingKeyId(fingerprint []byte, version int) uint64 { + switch version { + case 4: + return binary.BigEndian.Uint64(fingerprint[12:20]) + default: + panic("invalid pgp key version") + } +} diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index 1e92e22c9..0595a48ac 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -8,6 +8,7 @@ package packet // import "github.com/ProtonMail/go-crypto/openpgp/packet" import ( "bytes" + "crypto" "crypto/cipher" "crypto/rsa" "io" @@ -506,16 +507,31 @@ 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 = 30 + PubKeyAlgoMldsa87Ed448 = 31 + PubKeyAlgoSlhdsaShake128s = 32 + PubKeyAlgoSlhdsaShake128f = 33 + PubKeyAlgoSlhdsaShake256s = 34 ) // 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,12 +541,26 @@ 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, + PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: return true } return false } +// HandleSpecificHash returns the mandated hash if the algorithm requires it; +// otherwise, it returns the selectedHash. +func (pka PublicKeyAlgorithm) HandleSpecificHash(selectedHash crypto.Hash) crypto.Hash { + switch pka { + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f: + return crypto.SHA3_256 + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake256s: + return crypto.SHA3_512 + } + return selectedHash +} + // CipherFunction represents the different block ciphers specified for OpenPGP. See // http://www.iana.org/assignments/pgp-parameters/pgp-parameters.xhtml#pgp-parameters-13 type CipherFunction algorithm.CipherFunction diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index f04e6c6b8..05fd909ce 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -13,6 +13,7 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" + goerrors "errors" "fmt" "io" "math/big" @@ -27,7 +28,11 @@ 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/slhdsa" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" "golang.org/x/crypto/hkdf" @@ -166,6 +171,12 @@ func NewSignerPrivateKey(creationTime time.Time, signer interface{}) *PrivateKey pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) case ed448.PrivateKey: pk.PublicKey = *NewEd448PublicKey(creationTime, &pubkey.PublicKey) + case *symmetric.HMACPrivateKey: + pk.PublicKey = *NewHMACPublicKey(creationTime, &pubkey.PublicKey) + case *mldsa_eddsa.PrivateKey: + pk.PublicKey = *NewMldsaEddsaPublicKey(creationTime, &pubkey.PublicKey) + case *slhdsa.PrivateKey: + pk.PublicKey = *NewSlhdsaPublicKey(creationTime, &pubkey.PublicKey) default: panic("openpgp: unknown signer type in NewSignerPrivateKey") } @@ -173,7 +184,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 +198,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 +545,58 @@ 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-05.html#name-key-material-packets +func serializeMlkemPrivateKey(w io.Writer, priv *mlkem_ecdh.PrivateKey) (err error) { + if _, err = w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + _, err = w.Write(encoding.NewOctetArray(priv.SecretMlkemSeed).EncodedBytes()) + return err +} + +// serializeMldsaEddsaPrivateKey serializes a ML-DSA + EdDSA private key according to +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets-2 +func serializeMldsaEddsaPrivateKey(w io.Writer, priv *mldsa_eddsa.PrivateKey) error { + if _, err := w.Write(encoding.NewOctetArray(priv.SecretEc).EncodedBytes()); err != nil { + return err + } + if _, err := w.Write(encoding.NewOctetArray(priv.SecretMldsaSeed).EncodedBytes()); err != nil { + return err + } + return nil +} + +// serializeSlhDsaPrivateKey serializes a SLH-DSA private key. +func serializeSlhDsaPrivateKey(w io.Writer, priv *slhdsa.PrivateKey) error { + marshalledKey, err := priv.SecretSlhdsa.MarshalBinary() + if err != nil { + return err + } + if _, err := w.Write(marshalledKey); err != nil { + return err + } + return nil +} + // decrypt decrypts an encrypted private key using a decryption key. func (pk *PrivateKey) decrypt(decryptionKey []byte) error { if pk.Dummy() { @@ -830,6 +897,16 @@ 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) + case *slhdsa.PrivateKey: + err = serializeSlhDsaPrivateKey(w, priv) default: err = errors.InvalidArgumentError("unknown private key type") } @@ -858,6 +935,20 @@ 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, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMlkem1024X448: + return pk.parseMlkemEcdhPrivateKey(data, 56, mlkem_ecdh.MlKemSeedLen) + case PubKeyAlgoMldsa65Ed25519: + return pk.parseMldsaEddsaPrivateKey(data, 32, mldsa_eddsa.MlDsaSeedLen) + case PubKeyAlgoMldsa87Ed448: + return pk.parseMldsaEddsaPrivateKey(data, 57, mldsa_eddsa.MlDsaSeedLen) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + return pk.parseSlhdsaPrivateKey(data) default: err = errors.StructuralError("unknown private key type") return @@ -1121,6 +1212,153 @@ 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-05.html#name-key-material-packets-2 +func (pk *PrivateKey) parseMldsaEddsaPrivateKey(data []byte, ecLen, seedLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-DSA + EdDSA key") + } + pub := pk.PublicKey.PublicKey.(*mldsa_eddsa.PublicKey) + priv := new(mldsa_eddsa.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlDsaKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mldsa_eddsa.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseMlkemEcdhPrivateKey parses a ML-KEM + ECC private key as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-key-material-packets +func (pk *PrivateKey) parseMlkemEcdhPrivateKey(data []byte, ecLen, seedLen int) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 ML-KEM + ECDH key") + } + pub := pk.PublicKey.PublicKey.(*mlkem_ecdh.PublicKey) + priv := new(mlkem_ecdh.PrivateKey) + priv.PublicKey = *pub + + buf := bytes.NewBuffer(data) + ec := encoding.NewEmptyOctetArray(ecLen) + if _, err := ec.ReadFrom(buf); err != nil { + return err + } + priv.SecretEc = ec.Bytes() + + seed := encoding.NewEmptyOctetArray(seedLen) + if _, err := seed.ReadFrom(buf); err != nil { + return err + } + if err = priv.DeriveMlKemKeys(seed.Bytes(), false); err != nil { + return err + } + + if err := mlkem_ecdh.Validate(priv); err != nil { + return err + } + pk.PrivateKey = priv + + return nil +} + +// parseSlhdsaPrivateKey parses a SLH-DSA private key. +func (pk *PrivateKey) parseSlhdsaPrivateKey(data []byte) (err error) { + if pk.Version != 6 { + return goerrors.New("openpgp: cannot parse non-v6 SLH-DSA key") + } + parsedPublicKey := pk.PublicKey.PublicKey.(*slhdsa.PublicKey) + parsedPrivateKey := new(slhdsa.PrivateKey) + parsedPrivateKey.PublicKey = *parsedPublicKey + parsedPrivateKey.SecretSlhdsa, err = parsedPrivateKey.Slhdsa.UnmarshalBinaryPrivateKey(data) + if err != nil { + return goerrors.New("openpgp: failed to unmarshal SLH-DSA key") + } + + if err := slhdsa.Validate(parsedPrivateKey); err != nil { + return err + } + pk.PrivateKey = parsedPrivateKey + + return nil +} + func validateDSAParameters(priv *dsa.PrivateKey) error { p := priv.P // group prime q := priv.Q // subgroup order diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f8da781bb..56dd0958c 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -5,12 +5,15 @@ package packet import ( + "bytes" + "crypto" "crypto/dsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" _ "crypto/sha512" "encoding/binary" + goerrors "errors" "fmt" "hash" "io" @@ -28,8 +31,19 @@ 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/slhdsa" + "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" + slhdsaCircl "github.com/cloudflare/circl/sign/slhdsa" ) // PublicKey represents an OpenPGP public key. See RFC 4880, section 5.5.2. @@ -37,7 +51,7 @@ type PublicKey struct { Version int CreationTime time.Time PubKeyAlgo PublicKeyAlgorithm - PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey + PublicKey interface{} // *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or *eddsa.PublicKey, *x25519.PublicKey, *x448.PublicKey, *ed25519.PublicKey, *ed448.PublicKey, or *mlkem_ecdh.PublicKey Fingerprint []byte KeyId uint64 IsSubkey bool @@ -69,6 +83,26 @@ func (pk *PublicKey) UpgradeToV6() error { return pk.checkV6Compatibility() } +// ReplaceKDF replaces the KDF instance, and updates all necessary fields. +func (pk *PublicKey) ReplaceKDF(kdf ecdh.KDF) error { + ecdhKey, ok := pk.PublicKey.(*ecdh.PublicKey) + if !ok { + return goerrors.New("wrong forwarding sub key generation") + } + + ecdhKey.KDF = kdf + byteBuffer := new(bytes.Buffer) + err := kdf.Serialize(byteBuffer) + if err != nil { + return err + } + + pk.kdf = encoding.NewOID(byteBuffer.Bytes()[1:]) + pk.setFingerprintAndKeyId() + + return nil +} + // signingKey provides a convenient abstraction over signature verification // for v3 and v4 public keys. type signingKey interface { @@ -230,6 +264,82 @@ 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 NewSlhdsaPublicKey(creationTime time.Time, pub *slhdsa.PublicKey) *PublicKey { + publicKeyBytes, err := pub.PublicSlhdsa.MarshalBinary() + if err != nil { + panic(err) + } + pk := &PublicKey{ + Version: 6, + CreationTime: creationTime, + PubKeyAlgo: PublicKeyAlgorithm(pub.AlgId), + PublicKey: pub, + q: encoding.NewOctetArray(publicKeyBytes), + } + + pk.setFingerprintAndKeyId() + return pk +} + func (pk *PublicKey) parse(r io.Reader) (err error) { // RFC 4880, section 5.5.2 var buf [6]byte @@ -258,7 +368,7 @@ func (pk *PublicKey) parse(r io.Reader) (err error) { } pk.CreationTime = time.Unix(int64(uint32(buf[1])<<24|uint32(buf[2])<<16|uint32(buf[3])<<8|uint32(buf[4])), 0) pk.PubKeyAlgo = PublicKeyAlgorithm(buf[5]) - // Ignore four-ocet length + // Ignore four-octet length switch pk.PubKeyAlgo { case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoRSASignOnly: err = pk.parseRSA(r) @@ -280,6 +390,20 @@ 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) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + err = pk.parseSlhDsa(r) default: err = errors.UnsupportedError("public key type: " + strconv.Itoa(int(pk.PubKeyAlgo))) } @@ -474,11 +598,13 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) { return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid)) } - if kdfLen := len(pk.kdf.Bytes()); kdfLen < 3 { + kdfLen := len(pk.kdf.Bytes()) + if kdfLen < 3 { return errors.UnsupportedError("unsupported ECDH KDF length: " + strconv.Itoa(kdfLen)) } - if reserved := pk.kdf.Bytes()[0]; reserved != 0x01 { - return errors.UnsupportedError("unsupported KDF reserved field: " + strconv.Itoa(int(reserved))) + kdfVersion := int(pk.kdf.Bytes()[0]) + if kdfVersion != ecdh.KDFVersion1 && kdfVersion != ecdh.KDFVersionForwarding { + return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(kdfVersion)) } kdfHash, ok := algorithm.HashById[pk.kdf.Bytes()[1]] if !ok { @@ -489,9 +615,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-05.html#name-key-material-packets +func (pk *PublicKey) parseMlkemEcdh(r io.Reader, ecLen, kLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(kLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mlkem_ecdh.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetECDHCurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mlkem, err = GetMlkemFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMlkem, err = pub.Mlkem.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub return } @@ -594,6 +768,115 @@ 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-05.html#name-key-material-packets-2 +func (pk *PublicKey) parseMldsaEddsa(r io.Reader, ecLen, dLen int) (err error) { + pk.p = encoding.NewEmptyOctetArray(ecLen) + if _, err = pk.p.ReadFrom(r); err != nil { + return + } + + pk.q = encoding.NewEmptyOctetArray(dLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return + } + + pub := &mldsa_eddsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + PublicPoint: pk.p.Bytes(), + } + + if pub.Curve, err = GetEdDSACurveFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.Mldsa, err = GetMldsaFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + if pub.PublicMldsa, err = pub.Mldsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = pub + return +} + +func (pk *PublicKey) parseSlhDsa(r io.Reader) (err error) { + parsedPublicKey := &slhdsa.PublicKey{ + AlgId: uint8(pk.PubKeyAlgo), + } + + if parsedPublicKey.Slhdsa, err = GetSlhdsaSchemeFromAlgID(pk.PubKeyAlgo); err != nil { + return err + } + + keyLen := parsedPublicKey.Slhdsa.PublicKeySize() + pk.q = encoding.NewEmptyOctetArray(keyLen) + if _, err = pk.q.ReadFrom(r); err != nil { + return err + } + + if parsedPublicKey.PublicSlhdsa, err = parsedPublicKey.Slhdsa.UnmarshalBinaryPublicKey(pk.q.Bytes()); err != nil { + return err + } + + pk.PublicKey = parsedPublicKey + return nil +} + // SerializeForHash serializes the PublicKey to w with the special packet // header format needed for hashing. func (pk *PublicKey) SerializeForHash(w io.Writer) error { @@ -681,6 +964,15 @@ 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()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + length += uint32(pk.q.EncodedLength()) default: panic("unknown public key algorithm") } @@ -773,13 +1065,39 @@ 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 + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + _, err = w.Write(pk.q.EncodedBytes()) + return } return errors.InvalidArgumentError("bad public-key algorithm") } // CanSign returns true iff this public key can generate signatures func (pk *PublicKey) CanSign() bool { - return pk.PubKeyAlgo != PubKeyAlgoRSAEncryptOnly && pk.PubKeyAlgo != PubKeyAlgoElGamal && pk.PubKeyAlgo != PubKeyAlgoECDH + return pk.PubKeyAlgo.CanSign() } // VerifyHashTag returns nil iff sig appears to be a plausible signature of the data @@ -859,6 +1177,41 @@ 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: + if pk.PubKeyAlgo == PubKeyAlgoMldsa65Ed25519 && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: Mldsa65Ed25519 requires sha3-256 message hash: has %s", sig.Hash)) + } + if pk.PubKeyAlgo == PubKeyAlgoMldsa87Ed448 && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: Mldsa87Ed448 requires sha3-512 message hash: has %s", sig.Hash)) + } + mldsaEddsaPublicKey := pk.PublicKey.(*mldsa_eddsa.PublicKey) + if !mldsa_eddsa.Verify(mldsaEddsaPublicKey, hashBytes, sig.MldsaSig.Bytes(), sig.EdDSASigR.Bytes()) { + return errors.SignatureError("MldsaEddsa verification failure") + } + return nil + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if (pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake128s || pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake128f) && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake128 requires sha3-256 message hash: has %s", sig.Hash)) + } + if pk.PubKeyAlgo == PubKeyAlgoSlhdsaShake256s && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash)) + } + slhDsaPublicKey := pk.PublicKey.(*slhdsa.PublicKey) + if !slhdsa.Verify(slhDsaPublicKey, hashBytes, sig.SlhdsaSig.Bytes()) { + return errors.SignatureError("MldsaEddsa verification failure") + } + return nil default: return errors.SignatureError("Unsupported public key algorithm used in signature") } @@ -929,6 +1282,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 } @@ -1080,6 +1440,13 @@ 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. + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + bitLength = pk.q.BitLength() default: err = errors.InvalidArgumentError("bad public-key algorithm") } @@ -1118,3 +1485,86 @@ 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, PubKeyAlgoSlhdsaShake128s, + PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + return true + default: + return false + } +} + +func GetMatchingMlkem(algId PublicKeyAlgorithm) (PublicKeyAlgorithm, error) { + switch algId { + case PubKeyAlgoMldsa65Ed25519, PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f: + return PubKeyAlgoMlkem768X25519, nil + case PubKeyAlgoMldsa87Ed448, PubKeyAlgoSlhdsaShake256s: + 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") + } +} + +// GetSlhdsaSchemeFromAlgID returns the SLH-DSA instance from the matching KEM +func GetSlhdsaSchemeFromAlgID(algId PublicKeyAlgorithm) (sign.Scheme, error) { + switch algId { + case PubKeyAlgoSlhdsaShake128s: + return slhdsaCircl.ParamIDSHAKESmall128, nil + case PubKeyAlgoSlhdsaShake128f: + return slhdsaCircl.ParamIDSHAKEFast128, nil + case PubKeyAlgoSlhdsaShake256s: + return slhdsaCircl.ParamIDSHAKESmall256, nil + default: + return nil, goerrors.New("packet: unsupported SLH-DSA public key algorithm") + } +} + +// GetECDHCurveFromAlgID returns the ECDH curve instance from the matching KEM +func GetECDHCurveFromAlgID(algId PublicKeyAlgorithm) (ecc.ECDHCurve, error) { + switch algId { + 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 3a4b366d8..7904980a6 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -10,6 +10,7 @@ import ( "crypto/dsa" "encoding/asn1" "encoding/binary" + "fmt" "hash" "io" "math/big" @@ -23,6 +24,10 @@ 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/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + "github.com/cloudflare/circl/sign/mldsa/mldsa87" ) const ( @@ -34,7 +39,7 @@ const ( KeyFlagEncryptStorage KeyFlagSplitKey KeyFlagAuthenticate - _ + KeyFlagForward KeyFlagGroupKey ) @@ -80,7 +85,10 @@ type Signature struct { DSASigR, DSASigS encoding.Field ECDSASigR, ECDSASigS encoding.Field EdDSASigR, EdDSASigS encoding.Field + HMAC encoding.Field EdSig []byte + MldsaSig encoding.Field + SlhdsaSig encoding.Field // rawSubpackets contains the unparsed subpackets, in order. rawSubpackets []outputSubpacket @@ -127,8 +135,9 @@ type Signature struct { // FlagsValid is set if any flags were given. See RFC 9580, section // 5.2.3.29 for details. - FlagsValid bool - FlagCertify, FlagSign, FlagEncryptCommunications, FlagEncryptStorage, FlagSplitKey, FlagAuthenticate, FlagGroupKey bool + FlagsValid bool + FlagCertify, FlagSign, FlagEncryptCommunications, FlagEncryptStorage bool + FlagSplitKey, FlagAuthenticate, FlagForward, FlagGroupKey bool // RevocationReason is set if this signature has been revoked. // See RFC 9580, section 5.2.3.31 for details. @@ -198,7 +207,9 @@ 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, + PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: default: err = errors.UnsupportedError("public key algorithm " + strconv.Itoa(int(sig.PubKeyAlgo))) return @@ -336,12 +347,53 @@ 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 + } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if err = sig.parseSlhdsaSignature(r, sig.PubKeyAlgo); err != nil { + return + } default: panic("unreachable") } return } +// parseMldsaEddsaSignature parses an ML-DSA + EdDSA signature as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-05.html#name-signature-packet-tag-2 +func (sig *Signature) parseMldsaEddsaSignature(r io.Reader, ecLen, dLen int) (err error) { + sig.EdDSASigR = encoding.NewEmptyOctetArray(ecLen) + if _, err = sig.EdDSASigR.ReadFrom(r); err != nil { + return + } + + sig.MldsaSig = encoding.NewEmptyOctetArray(dLen) + _, err = sig.MldsaSig.ReadFrom(r) + return +} + +// parseSlhdsaSignature parses an SLH-DSA signature as specified in +func (sig *Signature) parseSlhdsaSignature(r io.Reader, algID PublicKeyAlgorithm) (err error) { + scheme, err := GetSlhdsaSchemeFromAlgID(algID) + if err != nil { + return err + } + sig.SlhdsaSig = encoding.NewEmptyOctetArray(scheme.SignatureSize()) + _, err = sig.SlhdsaSig.ReadFrom(r) + return +} + // parseSignatureSubpackets parses subpackets of the main signature packet. See // RFC 9580, section 5.2.3.1. func parseSignatureSubpackets(sig *Signature, subpackets []byte, isHashed bool) (err error) { @@ -582,6 +634,9 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r if subpacket[0]&KeyFlagAuthenticate != 0 { sig.FlagAuthenticate = true } + if subpacket[0]&KeyFlagForward != 0 { + sig.FlagForward = true + } if subpacket[0]&KeyFlagGroupKey != 0 { sig.FlagGroupKey = true } @@ -996,6 +1051,44 @@ 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.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + if priv.PubKeyAlgo == PubKeyAlgoMldsa65Ed25519 && sig.Hash != crypto.SHA3_256 { + return errors.StructuralError(fmt.Sprintf("Mldsa65Ed25519 requires sha3-256 message hash: got %s", sig.Hash)) + } + if priv.PubKeyAlgo == PubKeyAlgoMldsa87Ed448 && sig.Hash != crypto.SHA3_512 { + return errors.StructuralError(fmt.Sprintf("Mldsa87Ed448 requires sha3-512 message hash: got %s", sig.Hash)) + } + sk := priv.PrivateKey.(*mldsa_eddsa.PrivateKey) + dSig, ecSig, err := mldsa_eddsa.Sign(sk, digest) + + if err == nil { + sig.MldsaSig = encoding.NewOctetArray(dSig) + sig.EdDSASigR = encoding.NewOctetArray(ecSig) + } + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + if sig.Version != 6 { + return errors.StructuralError("cannot use MldsaEdDsa on a non-v6 signature") + } + if (priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake128s || priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake128f) && sig.Hash != crypto.SHA3_256 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake128 requires sha3-256 message hash: has %s", sig.Hash)) + } + if priv.PubKeyAlgo == PubKeyAlgoSlhdsaShake256s && sig.Hash != crypto.SHA3_512 { + return errors.SignatureError(fmt.Sprintf("verification failure: SlhDsaShake256 requires sha3-512 message hash: has %s", sig.Hash)) + } + sk := priv.PrivateKey.(*slhdsa.PrivateKey) + dSig, err := slhdsa.Sign(sk, digest) + + if err == nil { + sig.SlhdsaSig = encoding.NewOctetArray(dSig) + } default: err = errors.UnsupportedError("public key algorithm: " + strconv.Itoa(int(sig.PubKeyAlgo))) } @@ -1113,7 +1206,7 @@ func (sig *Signature) Serialize(w io.Writer) (err error) { if len(sig.outSubpackets) == 0 { sig.outSubpackets = sig.rawSubpackets } - if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil { + if sig.RSASignature == nil && sig.DSASigR == nil && sig.ECDSASigR == nil && sig.EdDSASigR == nil && sig.EdSig == nil && sig.SlhdsaSig == nil && sig.HMAC == nil { return errors.InvalidArgumentError("Signature: need to call Sign, SignUserId or SignKey before Serialize") } @@ -1134,6 +1227,13 @@ 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()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + sigLength += int(sig.SlhdsaSig.EncodedLength()) default: panic("impossible") } @@ -1240,6 +1340,15 @@ 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()) + case PubKeyAlgoSlhdsaShake128s, PubKeyAlgoSlhdsaShake128f, PubKeyAlgoSlhdsaShake256s: + _, err = w.Write(sig.SlhdsaSig.EncodedBytes()) default: panic("impossible") } @@ -1352,6 +1461,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 edc431142..19940387a 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -82,6 +82,33 @@ ltm2aQaG } } +func TestSymmetricSignatureRead(t *testing.T) { + const serializedPacket = "c272040165080006050260639e4e002109107fc6eeae2d3315b1162104e29ad49f0b7d0b12bb0401407fc6eeae2d3315b13adc400ecca603da8e6f3c82727ffc3e9416bc0236c9665498dda14f1c1dd4e4acacc7725d6dac7598e0951b5f1f8789714fb7fcdda4a9f10056134a7edf9d9a4fc45d" + expectedHMAC := []byte{0x0e, 0xcc, 0xa6, 0x03, 0xda, 0x8e, 0x6f, 0x3c, 0x82, 0x72, 0x7f, 0xfc, 0x3e, 0x94, 0x16, 0xbc, 0x02, 0x36, 0xc9, 0x66, 0x54, 0x98, 0xdd, 0xa1, 0x4f, 0x1c, 0x1d, 0xd4, 0xe4, 0xac, 0xac, 0xc7, 0x72, 0x5d, 0x6d, 0xac, 0x75, 0x98, 0xe0, 0x95, 0x1b, 0x5f, 0x1f, 0x87, 0x89, 0x71, 0x4f, 0xb7, 0xfc, 0xdd, 0xa4, 0xa9, 0xf1, 0x00, 0x56, 0x13, 0x4a, 0x7e, 0xdf, 0x9d, 0x9a, 0x4f, 0xc4, 0x5d} + + packet, err := Read(readerFromHex(serializedPacket)) + if err != nil { + t.Error(err) + } + + sig, ok := packet.(*Signature) + if !ok { + t.Errorf("Did not parse a signature packet") + } + + if sig.PubKeyAlgo != ExperimentalPubKeyAlgoHMAC { + t.Error("Wrong public key algorithm") + } + + if sig.Hash != crypto.SHA256 { + t.Error("Wrong public key algorithm") + } + + if !bytes.Equal(sig.HMAC.Bytes(), expectedHMAC) { + t.Errorf("Wrong HMAC value, got: %x, expected: %x\n", sig.HMAC.Bytes(), expectedHMAC) + } +} + func TestSignatureReserialize(t *testing.T) { packet, _ := Read(readerFromHex(signatureDataHex)) sig := packet.(*Signature) diff --git a/openpgp/read.go b/openpgp/read.go index e6dd9b5fd..10bc1899b 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -23,6 +23,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -118,7 +121,9 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + 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 23fd4aec1..2d2df3767 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "math/bits" "os" + "strconv" "strings" "testing" @@ -29,6 +30,13 @@ func readerFromHex(s string) io.Reader { return bytes.NewBuffer(data) } +func TestReadKeyRingWithSymmetricSubkey(t *testing.T) { + _, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey)) + if err != nil { + t.Error("could not read keyring", err) + } +} + func TestReadKeyRing(t *testing.T) { kring, err := ReadKeyRing(readerFromHex(testKeys1And2Hex)) if err != nil { @@ -770,7 +778,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) @@ -952,3 +960,90 @@ 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_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"bf262b24177002ac8ae5dc6da47c056d22ab9906d47d07952b75c358021901ca", "48b94bce2f9771788f5feb74122d599989c400cc0f49108bc98e0ea7945e4838"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + },*/ +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + secretKey, err := ReadArmoredKeyRing(strings.NewReader(test.armoredPrivateKey)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != test.armoredPublicKey { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessages { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgReader, err := armor.Decode(strings.NewReader(armoredMessage)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\r\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) + } +} diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 670d60226..5e2ae370b 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -455,3 +455,719 @@ 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 v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +//const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PublicTestVector = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +AAAAAAAAAAAABQwRGCAn +-----END PGP PUBLIC KEY BLOCK-----` + +const v6Ed25519Mlkem768X25519PrivateMessageTestVector = `-----BEGIN PGP MESSAGE----- + +wcPtBiEGSLlLzi+XcXiPX+t0Ei1ZmYnEAMwPSRCLyY4Op5ReSDhpDRKwy+pl +E3zYoBXhbdabEpLrdg9xijJjhEgcADYRJhjX5Eq2PWkfbvlyMrFb/PcOtyt3 +KgGcdqKaY0iX3Su0e59vk6ssDSgLUyZc9wOw4fRvPFR9sTr9GcGm2dzVobGx +Edm7h9BRNdGt6NjJPmP8qzpaMV49ePrsea3XYdBIa4//Ta5VCUWwQYIjBJNv +Gy74w6fFCVs4DXnPoz10HECOpdU6AuP44kBj5DR3G/zOW43gNTWjL1vGXutK +iM0DWR62Z8PP5FlCFZ41kJNxmObaDn4cHX2N393M7O5tiR0nSZlMPKlRI6Em +Mthp/e5NNv/li1gD+2b5U2VT53mhc78r+cv7Z2VFZ2/lPkouVFNnmoGFEuL2 +qnSnjCx12dgYlmpd3mRgbmJulAd41VGUYxTOUkhT7iXoONAR4hbwL2pCBStA +EwIoGuUJYttmQ4Z3Q6eFE+ctPriyF4BFSOImlc1xj+GQwfpX2UcHlwJ+9jXo +lPjva1XHLdSH9bNRT68acml3RG9Fjmp1NcjuDIw/kuNhxb3ffP4z8cfUH1vF +3FDMbEO71DM52EaO5sVOZzBHeN8JpACP7o/UqAV7J7YR1hX/C5usy0iUVi2y +tnf8ilqsgs0a6NYwqXRBLYnII4LHulHhMU/9tEQfl8Wb0EpqWmgmXmua9i5X +01V2fB3CBQ+wuEN71n34YOrwCgrUvB58QJQQmmYWNE9frVj5kbd9+Muu7278 +xW/OXmnEcRy0Nj8QftUUvD4c1EXBJ0E86OfYE51+Y8l8h55r7WZ268o1vvc+ +/Ky9hMbL/STKD1DIRgYL8VQTRsKE7Vnh6fgSGUJ8P6Ms0cS6JqRFXLHIHZuP +k3pN9freUmKKgWQU1EYVox78cIC2bDLxZcHGURalvjTiX49f66L/eBQt/Hl3 +aVbcat/x85A6pOj4YKZU6VaGrzAoe7+mLJIHeC5gV0mdOwBAvSFIaD9wqo8Q +o5D8OmSarJDMZYT/WYGrZ3Q2dxSAVE+3Vp18SGUrLkXb+w2OAS9S5HrA+B4P +6FTfC0cLn0j/lqToxoRSKNknFIN3M9hGL5JXBULBCNeDxclsND5zNuKEY903 +a9LqpLr9KfF/e2Qhowd2cip2IcGvwjWD8CiXw2JxRtZs8dTM2neYVOnSlag1 +COjRzonBrLj1/9MQWNBHPaD+hN5vvDeda+QhAGaDHOUUd16Ftl0VGuGqBjU3 +p5HfdOMnf5boxuFVXluk8oEi7MrbQbBYPFccFh4GzSMlt0eW8LFHEXezrtDZ +18OHEDZLxYnB/1V+X1EJnXaI0hlxuhBonjOjzL2CBe1T5JH5ZUR6/QK5UwLh ++U5V1j1r/o++b/VD3KZ76k6vU0dpCW9LCpJliikbJqTD8UDGKRZRcwA61aG0 +MDwljqMGWzm9C3MR39PfOxaMRSJqecu0zpcBjyAKJMnVI86RKRWxN/1G0dgr +DmVDj4jILCh+ON/3nqg7MlLYoLCMbNGbavwqwdK1hAUboSjW2WX48rP/y7x/ +9YQcR26VYatCvzyl0YOwR42ySHec/c0ePVFIXDda0usCCQIM4bk71VtrSLy5 +cC6aNQtuVdt/PKl8huduHAnoJVfypIE4wNn7P/HXE7Q+r0b3QT3bcnj+hcDR +9HgehQakKYp7MhWexcnn4zmDxMgMy5mWay+4gHZnOML3Ly90tZn08THxjU5l +6OhR5wQMP7Ry7g7wKNyyzWkKih/hS6IglIp69PcugcFLZ5ZfKY+WB9Z36f5x +mJ/DrfDpIuHc6Kpck73quFElHzsIbJGpKxlG5Ndo95p/p393bD26MSOrdTDg +8zSQ3twmVlNGPAexsvNAPEVAvhSvDkdGdMMJGVWYBtCnQLJg0jXZ2YPl9v8J +8jyFLtU7klaT5sWLjzQ1A0TH6/2IpAC164LsY4lfk7ICwPmnlxaD04gtf6yO +tXuQIQiXDweBnGGS89jqRt93fXZtJWWts9SZrYoCGSXej7tWFu4BR9d/JWoV +74RYmIqYkl90ZWBBnFsllTCjZWdAmYjCTBBdmO9uLvWxRfTiNzRWISJ6btfZ +orhAJ8F4iHpeE3csgUR1aBQJnpChBY10JIz2KKaRRcknp/kRyuXnm+j6zMW2 +lvfBEGuvShfyUu/Lr6LTeUL4mU8lzq2xFKikzxRZgV+5zcfxdZeIS1f6/mrF +R34rpb8alO7ATHE6dXIULwjN7KYsKkzQPHLHHv+tX8r4i5eSQmmS5xJGJGss +fsZQnHEDvqro1XCRy6ysEIqF/Z94nthxdYrwWJjcMbk5C8obGPRp4UPZkVF6 +s+z7VgUTlg3+DD5oTRJNc45CqV5gLrqvDvpr7wya7oQgLrtA2d4TMP7QNQfT +lVfvSKiDCC3PxgSYTrU+B7gBhTBWHirgOZR/KZO5n57UYJct7A/DSjf1prpE +JnZ7doW8jnFlcLTJ6cffP4kVkN/rf0TWaxPzDY4+aZHj6s6cuXZSEtbuMZgQ +DtiIAwOrtq0jg7SYpKHY8eKBfm5jqi4j6IJltmUQ6wCM9ldRwCe9z9c4j0rD +/4ax+Pyu6f4G1jLuUKufMkH9ntIiQlO148oUDiBvD3IOgaGRLQgkUwv0+g34 +Ayymdk8fxmm9D06fPX2YdPXduXMLfDkWy0KnzvbYtpGaO/w04jceirEWDTdW +yXSjHDcPXsqFT5rgpY7fVeFAg46qFREdXj10zS5G1at7/VhsH3h7sLkZtBJY +qN2W155wLzVQdU/bguFwnqibfeZjRDYajaxCyk7fz8wefYUF10enlbT41F2S +Z8swFwflJnumWsrIjLfijyxF+V2k+Q4ZzeW1cSvLiYx812uRHn59iI12y0Cc +fT31IwwF59rGa9qRzT5T9/xJwiBBWYZgveJM2er1L4ZO4xfPQKo7pUQQP/QD +g6J+qlYIeDmfBu3cgA/JPn/G74mj6uQshwOuRf/t2Cdif081dTyn/grwanxl +Dk5VAujPt/Dj6KaSRneZkin5aI1M8Jzxulwi7QvrOaO2qyDIHYG+aHa2WUAx +oB/2jVrA+D8NyRNf+7FkZQvVD54jxLWQ2KhnCB+T9ZTArlaFoSRF0Xri/43t +3V57JUiaZcTQatfSTS9jZFElMeWp05J6QBj2NrLdp1ViM0nb8H6eoImpO9BL +X3+3zDkVlZKinyDuG3yKTV5RAYujV2gzDkpDEZEt7NTeI5q9czeIMiV36ev4 +sL6FJzzV4cnmCW1gDEpkeah2xr+R9b74tXpzg7JQorzOvSrrKQ/jcUEnNnRx +lKgI8klRv+TBERFw39yHjy5EhhPtpqp9QMgA+rjS1wx0i4p4EEUH0UO8jRX5 +nBoEpE0gdOvPxFjtOcbUps1lE3/6epkEjW/reCV0lU+9+JczpKmgJmtrJQck +yJZixoPBA7nPuYaG/OQ6DmVtoYznd//vGbqBQwpPbG7jBRuL+0P1Qx3yZkGx +Rd80d+46xiNY8jSXoZcn1cpiwGJgyTTidZ2FMzgAnM/3RKr6vee8Qep8C6em +M8sXgZUMV1/ztp9QJfKOcKUVVy8r+rvtZoJZc6iMT2FxXvSOBrLrLA0FeM2d +s2tjfXNZSawaLAMjyalYt6bdxXhmx+sVoyUn30Y22ezQj+Xbwlz6p/RNFg/r +DnM7G6z6+juZa7gxbu8GcZcWf+NDrQVQUMAPhBYjdwJ3d3r4GGbXdG/sxvhv +UMPLGgCY3WLUlna6qeuk4DTeGffF8odVMVMXDvN/A2vMhmuosqnrRgkrcsAa +/BJrzytYA4a2I09+YmUooisI5V6SkbNUpnHG1TgQ9bD3Kc5dNVKWrl5QGcri +8b+PZ6EUj8ta6xQmi0D546SJ03wUZE1AB31wo7Xq272GJtiCY6yIYS74HFUX +1cH0xduitzJJXI0WNOlfzkS1HhZTIxtF9kaPGNdVGmxKywQfdyRIl3A8FRqo +PfylXokuduUNEJxH7sa3usUExJfkBIIlPQOnYruuqKExboHrqoemrfxNpGKv +tKxdFq+gUuQNX4Ue4sTJ77KN7pvavyD0alLwhgw5GEZJspaQnMWfbpOKqeZk +ImClhcX5OHz95SGk/tgwY4rBRzUlJTpIKrEia13dpJ5KQrImbPXq/aG+IuNs +9mhtrh8tytNVuagIwwfsdZc6b7lkMMPtEblkahg83LBKW7EOGmQWBl/OE5kn +7tqucrGyD9aV/FWR5MEkPYwXC/ynkZ9uGUYzJIhRH3KU+7xDs9WIQ8DZZUSz +17Kaise5XSPTHdyn6GnHnedjIWFOjsN9RNaoEGmpTZTgTZje1Q4LJD2v1YA1 +LjAQJ5n6mzZpecVFFXb6C+vb2gDOfjc2tfCb18P7z8EpVRtitPmpKM/fBcYO +sm1OKcv3ODXzHeNTyrdpfhALgWyVr9QbD4TolBlqh4KjlrIdREngkj+dZALM +rc7SybHtezeFyJJWiRqwQUwZWP2NpteicAwcAlKnrwiE0s7v/HSGnww3Tai4 +7n1bFirYp3akSHCRnw+TPSbCovyWsQ7ubTtLGqkima/fiUkEsqExN/Y6YZnB +t8Kr7COu/ZcPimm0L9Apf9/yojdvmyi6FWKTV3J99PcIRiXaNZEr6aa/l8Su +r3BepBxL+PEo7iO+usqPGnEPb+DnHHmhJNiu2jyHr27s69lnkriXlsqEgZcP +UQaDcWj2BY/o0B6XYBLBqWoPVq4Vz7Pua5G+PHbIngly6ox/BboO+7YVkq24 +VQO0ppfqPA/06K0MM81RFhmBHBBW8S1fhN4wVdJ4lnta2QDOeqgCHzerCIZN +aMYzG3wFEFKxwc/pQ//wdAEaSQw7TgldnEPYgrayB13rdQd3Lfuo+//ak8ls +D4GcgomF3xCpy5eMLOqznnXRR2j1V0tmD1QFiYmhIun9bZkpq4ZxfjtlRWyf +wwTWFbkPWVlE4ytF09sK8YTvqUF8BX2U3xepYNPLQ7X6d9mOxUAsZRfxN9Ls +3C77gkXFAev9QYtrdA6mTuDxzKH+uNUrJ+bOlQZRaloro3+jfPfiMCGNVJoJ +TDW1faBquewrd6Y822wc6Rgws6blr7AG3UNu+iBKhc4APzHBk4p2HSc5ccgr +ur63qRUoJtUzDl48iIe84vbPWYbSvHixwnPQDBW3ZihSJGRiUVXjA7PIhwlz +RU8APwp7/aihNgTCy/HDsYM9/DMxVd70XWOHa60snMIc8zMc5uClHm1jXF2i +4ILTj3hm7C21pyeiS6+G3e4qK5BOMDFIqLHAmMv+qn2ajrlgltTehKMV74DH +8l43EYQ4LHvrxd+OckEgVAxE5reRQSbGsxRQgMY7m1j5DQe3BohqTJMYlwcO +s3DctjzhVFJB2Uc08RJIZxFDjNvRjg7KH/w/aztV0synQmTmJZ5e6cu1Nhfx +b+763UHtd8KcrE1OJhBshN96+hKBPpUfDBSW/kr5WwVdOwuF2aszfMPEgiDU +6FGFZv9P1of6x1vme1I6B0u48rsApz1Bkoxny9yEDC08mn9FZ8Yb6OiTIwfG +6AGuBQe4FsDSjBsjH3r1h7G1h9NEnWowu2jxrdTN7Eejvz902Wagf+84t4Bm +FO+R0VXIzb03IzdJgAHufMaHb+RFRQ0nxM6mdPW4z5Qy4MxKk/5mN6GrVYL4 +YQOuGq8Ot46hm04QJ31Ibo9f7pCJeethjyGVfEL0ALGGmCXLXpAK9DpccRnW +RrARrw/P/vtcZhOMUNybOTAH2S2wTgVC5i9/OAwTHUcy9a6oAwhev25fO52R +sFsMsDQebnTmdgNVK52f0Mx4BygIjk6CSbxQRQrdnuXhCu8dNHWo77VsIvmW +h/5vjve+v+Fu5rO4SBAJxhgvHaGiBSz/m+av1SxZGn9ySSKJiUnm3gaKDl+I +OtXNFtA+9VElV+ude1W2ZV97Ph9NZo3LOksXnWzPxU1QZDQeuzFJdk9RTnrG +dp50HGnkMjLUIb0m6v03EdtZ3joIIma8gAdnpeeV9AIpsH2EHKyQ/Bm1QOZb +v93qajxmkzmVNhWvjsjp/oAskw6jYRup+dmTpI0UCpgWouM+vlrYn+41F8Hj +fkW68hMgCkJZYN4hYpdmESmvASkotnC7d47lqD23dmTQrwrCIz/gSmHcyyH7 +YaNf99Tz+hmQ0p8JYDbIkbFEN+5zwsxhA5r93m+VOIhBtB+Bpd2FNlC+5OZZ +J4oC9u2dUY6MR7C9xCruXDpjUsmghGo+eiT6NJfgAlFQq+g46Xva6iAJOw1F +uXacu6GOEshVRzgSL2/3fAiT4N6GDnLG3XtyxFDU+MIqAQrg3xAAnudEK8nL +YITSY05r3Pku2oEA/p27f94f6d81Qr9n3nDT8IakkKdl0VEIsEW6VW03GV7K +jXoyrGhDDakI/1XygauU5DpNuhgApoA= +-----END PGP MESSAGE-----` diff --git a/openpgp/slhdsa/slhdsa.go b/openpgp/slhdsa/slhdsa.go new file mode 100644 index 000000000..4aee70491 --- /dev/null +++ b/openpgp/slhdsa/slhdsa.go @@ -0,0 +1,73 @@ +// Package slhdsa implements SLH-DSA-SHAKE, suitable for OpenPGP, experimental. +// It follows the specs https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html +package slhdsa + +import ( + goerrors "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/cloudflare/circl/sign" + "github.com/cloudflare/circl/sign/slhdsa" +) + +type PublicKey struct { + AlgId uint8 + Slhdsa sign.Scheme + PublicSlhdsa sign.PublicKey +} + +type PrivateKey struct { + PublicKey + SecretSlhdsa sign.PrivateKey +} + +// GenerateKey generates a SLH-DSA key. +func GenerateKey(rand io.Reader, algId uint8, scheme sign.Scheme) (priv *PrivateKey, err error) { + priv = new(PrivateKey) + + priv.PublicKey.AlgId = algId + priv.PublicKey.Slhdsa = scheme + + keySeed := make([]byte, scheme.SeedSize()) + if _, err = rand.Read(keySeed); err != nil { + return nil, err + } + priv.PublicKey.PublicSlhdsa, priv.SecretSlhdsa = priv.PublicKey.Slhdsa.DeriveKey(keySeed) + + return priv, nil +} + +// Sign generates a SLH-DSA signature. +func Sign(priv *PrivateKey, message []byte) (signature []byte, err error) { + // The specification of SLH-DSA [FIPS-205] prescribes an optional non-deterministic message randomizer. + // This is not used in this specification + options := slhdsa.SignatureOpts{ + PreHashID: slhdsa.NoPreHash, + IsDeterministic: true, + } + signature, err = priv.SecretSlhdsa.Sign(nil, message, &options) + if err != nil { + return nil, fmt.Errorf("slhdsa: unable to sign with SLH-DSA: %s", err) + } + if signature == nil { + return nil, goerrors.New("slhdsa: unable to sign with SLH-DSA") + } + + return signature, nil +} + +// Verify verifies the SLH-DSA signature. +func Verify(pub *PublicKey, message, dSig []byte) bool { + return pub.Slhdsa.Verify(pub.PublicSlhdsa, message, dSig, nil) +} + +// Validate checks that the public key corresponds to the private key +func Validate(priv *PrivateKey) (err error) { + if !priv.PublicSlhdsa.Equal(priv.SecretSlhdsa.Public()) { + return errors.KeyInvalidError("slhdsa: invalid public key") + } + + return nil +} diff --git a/openpgp/slhdsa/slhdsa_test.go b/openpgp/slhdsa/slhdsa_test.go new file mode 100644 index 000000000..07606699e --- /dev/null +++ b/openpgp/slhdsa/slhdsa_test.go @@ -0,0 +1,87 @@ +// Package slhdsa_test tests the SLH-DSA implementation suitable for OpenPGP, experimental. +package slhdsa_test + +import ( + "crypto/rand" + "io" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" +) + +var algorithms = map[string]packet.PublicKeyAlgorithm{ + "SLH-DSA-SHAKE-128s": packet.PubKeyAlgoSlhdsaShake128s, + "SLH-DSA-SHAKE-128f": packet.PubKeyAlgoSlhdsaShake128f, + "SLH-DSA-SHAKE-256s": packet.PubKeyAlgoSlhdsaShake256s, +} + +func TestValidate(t *testing.T) { + for asymmName, asymmAlgo := range algorithms { + t.Run(asymmName, func(t *testing.T) { + testValidateAlgo(t, asymmAlgo) + }) + } +} + +func TestSignVerify(t *testing.T) { + for asymmName, asymmAlgo := range algorithms { + t.Run(asymmName, func(t *testing.T) { + key := testGenerateKeyAlgo(t, asymmAlgo) + testSignVerifyAlgo(t, key) + }) + } +} + +func testValidateAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) { + key := testGenerateKeyAlgo(t, algId) + if err := slhdsa.Validate(key); err != nil { + t.Fatalf("valid key marked as invalid: %s", err) + } + + bin, err := key.PublicSlhdsa.MarshalBinary() + if err != nil { + t.Fatal(err) + } + bin[5] ^= 1 + key.PublicSlhdsa, err = key.Slhdsa.UnmarshalBinaryPublicKey(bin) //PublicKeyFromBytes(bin) + if err != nil { + t.Fatal(err) + } + + if err := slhdsa.Validate(key); err == nil { + t.Fatalf("failed to detect invalid key") + } +} + +func testGenerateKeyAlgo(t *testing.T, algId packet.PublicKeyAlgorithm) *slhdsa.PrivateKey { + scheme, err := packet.GetSlhdsaSchemeFromAlgID(algId) + if err != nil { + t.Errorf("error getting SLH-DSA scheme: %s", err) + } + + priv, err := slhdsa.GenerateKey(rand.Reader, uint8(algId), scheme) + if err != nil { + t.Fatal(err) + } + + return priv +} + +func testSignVerifyAlgo(t *testing.T, priv *slhdsa.PrivateKey) { + digest := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, digest[:]) + if err != nil { + t.Fatal(err) + } + + dSig, err := slhdsa.Sign(priv, digest) + if err != nil { + t.Errorf("error encrypting: %s", err) + } + + result := slhdsa.Verify(&priv.PublicKey, digest, dSig) + if !result { + t.Error("unable to verify message") + } +} diff --git a/openpgp/symmetric/aead.go b/openpgp/symmetric/aead.go new file mode 100644 index 000000000..b9d389dc6 --- /dev/null +++ b/openpgp/symmetric/aead.go @@ -0,0 +1,75 @@ +package symmetric + +import ( + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" + "io" +) + +type AEADPublicKey struct { + Cipher algorithm.CipherFunction + BindingHash [32]byte + Key []byte +} + +type AEADPrivateKey struct { + PublicKey AEADPublicKey + HashSeed [32]byte + Key []byte +} + +func AEADGenerateKey(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv, err = generatePrivatePartAEAD(rand, cipher) + if err != nil { + return + } + + priv.generatePublicPartAEAD(cipher) + return +} + +func generatePrivatePartAEAD(rand io.Reader, cipher algorithm.CipherFunction) (priv *AEADPrivateKey, err error) { + priv = new(AEADPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, cipher.KeySize()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *AEADPrivateKey) generatePublicPartAEAD(cipher algorithm.CipherFunction) (err error) { + priv.PublicKey.Cipher = cipher + + bindingHash := ComputeBindingHash(priv.HashSeed) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + copy(priv.PublicKey.BindingHash[:], bindingHash) + return +} + +func (pub *AEADPublicKey) Encrypt(rand io.Reader, data []byte, mode algorithm.AEADMode) (nonce []byte, ciphertext []byte, err error) { + block := pub.Cipher.New(pub.Key) + aead := mode.New(block) + nonce = make([]byte, aead.NonceSize()) + rand.Read(nonce) + ciphertext = aead.Seal(nil, nonce, data, nil) + return +} + +func (priv *AEADPrivateKey) Decrypt(nonce []byte, ciphertext []byte, mode algorithm.AEADMode) (message []byte, err error) { + + block := priv.PublicKey.Cipher.New(priv.Key) + aead := mode.New(block) + message, err = aead.Open(nil, nonce, ciphertext, nil) + return +} diff --git a/openpgp/symmetric/hmac.go b/openpgp/symmetric/hmac.go new file mode 100644 index 000000000..e9d61475c --- /dev/null +++ b/openpgp/symmetric/hmac.go @@ -0,0 +1,109 @@ +package symmetric + +import ( + "crypto" + "crypto/hmac" + "crypto/sha256" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" +) + +type HMACPublicKey struct { + Hash algorithm.Hash + BindingHash [32]byte + // While this is a "public" key, the symmetric key needs to be present here. + // Symmetric cryptographic operations use the same key material for + // signing and verifying, and go-crypto assumes that a public key type will + // be used for verification. Thus, this `Key` field must never be exported + // publicly. + Key []byte +} + +type HMACPrivateKey struct { + PublicKey HMACPublicKey + HashSeed [32]byte + Key []byte +} + +func HMACGenerateKey(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv, err = generatePrivatePartHMAC(rand, hash) + if err != nil { + return + } + + priv.generatePublicPartHMAC(hash) + return +} + +func generatePrivatePartHMAC(rand io.Reader, hash algorithm.Hash) (priv *HMACPrivateKey, err error) { + priv = new(HMACPrivateKey) + var seed [32]byte + _, err = rand.Read(seed[:]) + if err != nil { + return + } + + key := make([]byte, hash.Size()) + _, err = rand.Read(key) + if err != nil { + return + } + + priv.HashSeed = seed + priv.Key = key + return +} + +func (priv *HMACPrivateKey) generatePublicPartHMAC(hash algorithm.Hash) (err error) { + priv.PublicKey.Hash = hash + + bindingHash := ComputeBindingHash(priv.HashSeed) + copy(priv.PublicKey.BindingHash[:], bindingHash) + + priv.PublicKey.Key = make([]byte, len(priv.Key)) + copy(priv.PublicKey.Key, priv.Key) + return +} + +func ComputeBindingHash(seed [32]byte) []byte { + bindingHash := sha256.New() + bindingHash.Write(seed[:]) + + return bindingHash.Sum(nil) +} + +func (priv *HMACPrivateKey) Public() crypto.PublicKey { + return &priv.PublicKey +} + +func (priv *HMACPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + expectedMAC, err := calculateMAC(priv.PublicKey.Hash, priv.Key, digest) + if err != nil { + return + } + signature = make([]byte, len(expectedMAC)) + copy(signature, expectedMAC) + return +} + +func (pub *HMACPublicKey) Verify(digest []byte, signature []byte) (bool, error) { + expectedMAC, err := calculateMAC(pub.Hash, pub.Key, digest) + if err != nil { + return false, err + } + return hmac.Equal(expectedMAC, signature), nil +} + +func calculateMAC(hash algorithm.Hash, key []byte, data []byte) ([]byte, error) { + hashFunc := hash.HashFunc() + if !hashFunc.Available() { + return nil, errors.UnsupportedError("hash function") + } + + mac := hmac.New(hashFunc.New, key) + mac.Write(data) + + return mac.Sum(nil), nil +} diff --git a/openpgp/v2/forwarding.go b/openpgp/v2/forwarding.go new file mode 100644 index 000000000..1306c510c --- /dev/null +++ b/openpgp/v2/forwarding.go @@ -0,0 +1,159 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package v2 + +import ( + goerrors "errors" + + "github.com/ProtonMail/go-crypto/openpgp/ecdh" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e. +// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found, +// instead of ignoring them +func (e *Entity) NewForwardingEntity( + name, comment, email string, config *packet.Config, strict bool, +) ( + forwardeeKey *Entity, instances []packet.ForwardingInstance, err error, +) { + if e.PrimaryKey.Version != 4 { + return nil, nil, errors.InvalidArgumentError("unsupported key version") + } + + now := config.Now() + + if _, err = e.VerifyPrimaryKey(now, config); err != nil { + return nil, nil, err + } + + // Generate a new Primary key for the forwardee + config.Algorithm = packet.PubKeyAlgoEdDSA + config.Curve = packet.Curve25519 + + forwardeePrimaryPrivRaw, err := newSigner(config) + if err != nil { + return nil, nil, err + } + + primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw) + + forwardeeKey = &Entity{ + PrimaryKey: &primary.PublicKey, + PrivateKey: primary, + Identities: make(map[string]*Identity), + Subkeys: []Subkey{}, + } + + keyProperties := selectKeyProperties(now, config, primary) + err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, keyProperties) + if err != nil { + return nil, nil, err + } + + // Init empty instances + instances = []packet.ForwardingInstance{} + + // Handle all forwarder subkeys + for _, forwarderSubKey := range e.Subkeys { + // Filter flags + if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() { + continue + } + + forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now, config) + // Filter expiration & revokal + if err != nil { + continue + } + + if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH { + if strict { + return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)") + } else { + continue + } + } + + forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, errors.InvalidArgumentError("malformed key") + } + + err = forwardeeKey.addEncryptionSubkey(config, now, 0) + if err != nil { + return nil, nil, err + } + + forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1] + forwardeeSubKeySelfSig := forwardeeSubKey.Bindings[0].Packet + + forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey) + if !ok { + return nil, nil, goerrors.New("wrong forwarding sub key generation") + } + + instance := packet.ForwardingInstance{ + KeyVersion: 4, + ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint, + } + + instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey) + if err != nil { + return nil, nil, err + } + + kdf := ecdh.KDF{ + Version: ecdh.KDFVersionForwarding, + Hash: forwarderEcdhKey.KDF.Hash, + Cipher: forwarderEcdhKey.KDF.Cipher, + } + + // If deriving a forwarding key from a forwarding key + if forwarderSubKeySelfSig.FlagForward { + if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding { + return nil, nil, goerrors.New("malformed forwarder key") + } + kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint + } else { + kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint + } + + err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf) + if err != nil { + return nil, nil, err + } + + // Extract fingerprint after changing the KDF + instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint + + // 0x04 - This key may be used to encrypt communications. + forwardeeSubKeySelfSig.FlagEncryptCommunications = false + + // 0x08 - This key may be used to encrypt storage. + forwardeeSubKeySelfSig.FlagEncryptStorage = false + + // 0x10 - The private component of this key may have been split by a secret-sharing mechanism. + forwardeeSubKeySelfSig.FlagSplitKey = true + + // 0x40 - This key may be used for forwarded communications. + forwardeeSubKeySelfSig.FlagForward = true + + err = forwardeeSubKeySelfSig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config) + if err != nil { + return nil, nil, err + } + + // Append each valid instance to the list + instances = append(instances, instance) + } + + if len(instances) == 0 { + return nil, nil, errors.InvalidArgumentError("no valid subkey found") + } + + return forwardeeKey, instances, nil +} diff --git a/openpgp/v2/forwarding_test.go b/openpgp/v2/forwarding_test.go new file mode 100644 index 000000000..9a16273f4 --- /dev/null +++ b/openpgp/v2/forwarding_test.go @@ -0,0 +1,253 @@ +package v2 + +import ( + "bytes" + "crypto/rand" + goerrors "errors" + "io" + "io/ioutil" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZQRXoxYJKwYBBAHaRw8BAQdAhxdzZ8ZP1M4UcauXSGbts38KhhAZxHNRcChs +9H7danMAAQC4tHykQmFpnlvhLYJDDc4MJm68mUB9qUls34GgKkqKNw6FzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CiwQTFggAPQUCZQRXowkQizX+kwlYIwMW +IQTYm4qmQoyzTnG0eZKLNf6TCVgjAwIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAMsQ +AQD9UHMIU418Z10UQrymhbjkGq/PHCytaaneaq5oycpN/QD/UiK3aA4+HxWhX/F2 +VrvEKL5a2xyd1AKKQ2DInF3xUg3HcQRlBFejEgorBgEEAZdVAQUBAQdAep7x8ncL +ShzEgKL6h9MAJbgX2z3BBgSLeAdg/rczKngX/woJjSg9O4DzqQOtAvdhYkDoOCNf +QgUAAP9OMqK0IwNmshCtktDy1/RTeyPKT8ItHDFAZ1ReKMA5CA63wngEGBYIACoF +AmUEV6MJEIs1/pMJWCMDFiEE2JuKpkKMs05xtHmSizX+kwlYIwMCG1wAAC5EAP9s +AbYBf9NGv1NxJvU0n0K++k3UIGkw9xgGJa3VFHFKvwEAx0DZpTVpCkJmiOFAOcfu +cSvjlMyQwsC/hAAzQpcqvwE= +=8LJg +-----END PGP PRIVATE KEY BLOCK-----` + +const forwardedMessage = `-----BEGIN PGP MESSAGE----- + +wV4DKsXbtIU9/JMSAQdA/6+foCjeUhS7Xto3fimUi6pfMQ/Ft3caHkK/1i767isw +NvG8xRbjQ0sAE1IZVGE1MBcVhCIbHhqp0h2J479Zmfn/iP7hfomYxrkJ/6UMnlEo +0kABKyyfO3QVAzBBNeq6hH27uqzwLgjWVrpgY7dmWPv0goSSaqHUda0lm+8JNUuF +wssOJTwrSwQrX3ezy5D/h/E6 +=okS+ +-----END PGP MESSAGE-----` + +const forwardedPlaintext = "Message for Bob" + +const forwardingKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xYUEZJ7obRYJKwYBBAHaRw8BAQdA0rsiAXbk646zNSFtehSG8tXV+933gX9qdlcv +y3dsETr+CQEIRDbKlCJxPw4WjfCI1f90n4Kr4ymuStB7MLm/mh+IyheqJgLtD4ak +EhgPd3R4o9TjQnwNbHnIfPo+FBbuo9T8yfnGzz0RvpL/ReZOViVdzRtjaGFybGll +IDxjaGFybGllQHByb3Rvbi5tZT7CjwQTFggAQQUCZJ7obQmQr3ZWGFoRxXwWIQTQ +TSCJvfPq/1Z83TKvdlYYWhHFfAIbAwIeAQIZAQMLCQcCFQgDFgACBScJAgcCAACM +OgD/cEsqqZdYl/RvYG3Kew658THsRFSGKeoEOZMvC0Ubza8BAIk6/dJNIYVvEBne +gCHO0yCfIITw5pH4SoF3okqOdaIKx54EZJ7obRIKKwYBBAGXVQEFAQEHQPNm6WCv +WZOZVKx0pYZJPWDxA1BfUrHStlBiaPqWHPkmF/8KCQ2qVg8YlFj8Z6f13kH8i+iY +FuX1/gkBCEQ2ypQicT8Oyr4aomc4TdKzvSb+xZA6xYugIUFzV4ojuS9UAuOB6yd2 +Ye66Exx6qz3kpxcDgbcf3ZRO/ljZT8XWItM7j/wiUrjxuxHw4cJ4BBgWCAAqBQJk +nuhtCZCvdlYYWhHFfBYhBNBNIIm98+r/VnzdMq92VhhaEcV8AhtQAADBagD+IrnW +ecLlUsQEhs4brBFXTpF5jy0p/aAjJ9AkNoYvS9YA/27VaHCJzZwJsc7HQWOxQB+V +gZt8hzaHXTuA3JwjuKEB +=DPb7 +-----END PGP PRIVATE KEY BLOCK-----` + +func TestForwardingStatic(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey)) + if err != nil { + t.Error(err) + return + } + + ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage)) + if err != nil { + t.Error(err) + return + } + + m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil) + if err != nil { + t.Fatal(err) + } + + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, []byte(forwardedPlaintext)) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingFull(t *testing.T) { + keyConfig := &packet.Config{ + Algorithm: packet.PubKeyAlgoEdDSA, + Curve: packet.Curve25519, + } + + plaintext := make([]byte, 1024) + rand.Read(plaintext) + + bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig) + if err != nil { + t.Fatal(err) + } + + charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity) + + if len(instances) != 1 { + t.Fatalf("invalid number of instances, expected 1 got %d", len(instances)) + } + + if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint) + } + + if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) { + t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint) + } + + // Encrypt message + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + _, err = w.Write(plaintext) + if err != nil { + t.Fatal(err) + } + + err = w.Close() + if err != nil { + t.Fatal(err) + } + + encrypted := buf.Bytes() + + // Decrypt message for Bob + m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil) + if err != nil { + t.Fatal(err) + } + dec, err := ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("decrypted does not match original") + } + + // Forward message + transformed := transformTestMessage(t, encrypted, instances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } + + // Setup further forwarding + danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true) + if err != nil { + t.Fatal(err) + } + + danielEntity = serializeAndParseForwardeeKey(t, danielEntity) + + secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0]) + + // Decrypt forwarded message for Charles + m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil) + if err != nil { + t.Fatal(err) + } + + dec, err = ioutil.ReadAll(m.decrypted) + + if !bytes.Equal(dec, plaintext) { + t.Fatal("forwarded decrypted does not match original") + } +} + +func TestForwardingKeyNotEncrypt(t *testing.T) { + charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardingKey)) + if err != nil { + t.Error(err) + return + } + if _, ok := charlesKey[0].EncryptionKey(time.Time{}, nil); ok { + t.Fatal("Marked forwarding keys should not be usable for encryption") + } +} + +func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte { + bytesReader := bytes.NewReader(encrypted) + packets := packet.NewReader(bytesReader) + splitPoint := int64(0) + transformedEncryptedKey := bytes.NewBuffer(nil) + +Loop: + for { + p, err := packets.Next() + if goerrors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("error in parsing message: %s", err) + } + switch p := p.(type) { + case *packet.EncryptedKey: + tp, err := p.ProxyTransform(instance) + if err != nil { + t.Fatalf("error transforming PKESK: %s", err) + } + + splitPoint = bytesReader.Size() - int64(bytesReader.Len()) + + err = tp.Serialize(transformedEncryptedKey) + if err != nil { + t.Fatalf("error serializing transformed PKESK: %s", err) + } + break Loop + } + } + + transformed := transformedEncryptedKey.Bytes() + transformed = append(transformed, encrypted[splitPoint:]...) + + return transformed +} + +func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity { + serializedEntity := bytes.NewBuffer(nil) + err := key.SerializePrivateWithoutSigning(serializedEntity, nil) + if err != nil { + t.Fatalf("Error in serializing forwardee key: %s", err) + } + el, err := ReadKeyRing(serializedEntity) + if err != nil { + t.Fatalf("Error in reading forwardee key: %s", err) + } + + if len(el) != 1 { + t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el)) + } + + return el[0] +} diff --git a/openpgp/v2/key_generation.go b/openpgp/v2/key_generation.go index c15eba9bb..1ac8a841c 100644 --- a/openpgp/v2/key_generation.go +++ b/openpgp/v2/key_generation.go @@ -21,7 +21,11 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/ecc" + "github.com/ProtonMail/go-crypto/openpgp/mldsa_eddsa" + "github.com/ProtonMail/go-crypto/openpgp/mlkem_ecdh" "github.com/ProtonMail/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/slhdsa" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" "github.com/ProtonMail/go-crypto/openpgp/x25519" "github.com/ProtonMail/go-crypto/openpgp/x448" ) @@ -399,6 +403,35 @@ 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) + case packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + if !config.V6() { + return nil, goerrors.New("openpgp: cannot create a non-v6 SLH-DSH key") + } + + d, err := packet.GetSlhdsaSchemeFromAlgID(config.PublicKeyAlgorithm()) + if err != nil { + return nil, err + } + + return slhdsa.GenerateKey(config.Random(), uint8(config.PublicKeyAlgorithm()), d) default: return nil, errors.InvalidArgumentError("unsupported public key algorithm") } @@ -406,6 +439,7 @@ func newSigner(config *packet.Config) (signer interface{}, err error) { // newDecrypter generates an encryption/decryption key. func newDecrypter(config *packet.Config) (decrypter interface{}, err error) { + pubKeyAlgo := config.PublicKeyAlgorithm() switch config.PublicKeyAlgorithm() { case packet.PubKeyAlgoRSA: bits := config.RSAModulusBits() @@ -441,6 +475,30 @@ 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, + packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f, packet.PubKeyAlgoSlhdsaShake256s: + 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 93082dd48..e2d3b4cc1 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -111,6 +111,7 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) // Iterate the keys to find the newest, unexpired one candidateSubkey := -1 + isPQ := false var maxTime time.Time var selectedSubkeySelfSig *packet.Signature for i, subkey := range e.Subkeys { @@ -118,10 +119,11 @@ func (e *Entity) EncryptionKey(now time.Time, config *packet.Config) (Key, bool) if err == nil && isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo) && checkKeyRequirements(subkey.PublicKey, config) == nil && - (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) { + (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix() || (!isPQ && subkey.IsPQ())) { candidateSubkey = i selectedSubkeySelfSig = subkeySelfSig maxTime = subkeySelfSig.CreationTime + isPQ = subkey.IsPQ() // Prefer PQ keys } } @@ -164,12 +166,12 @@ func (e *Entity) DecryptionKeys(id uint64, date time.Time, config *packet.Config for _, subkey := range e.Subkeys { subkeySelfSig, err := subkey.LatestValidBindingSignature(date, config) if err == nil && - (config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && + (config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && (id == 0 || subkey.PublicKey.KeyId == id) { keys = append(keys, Key{subkey.Primary, primarySelfSignature, subkey.PublicKey, subkey.PrivateKey, subkeySelfSig}) } } - if config.AllowDecryptionWithSigningKeys() || isValidEncryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { + if config.AllowDecryptionWithSigningKeys() || isValidDecryptionKey(primarySelfSignature, e.PrimaryKey.PubKeyAlgo) { keys = append(keys, Key{e, primarySelfSignature, e.PrimaryKey, e.PrivateKey, primarySelfSignature}) } return @@ -213,6 +215,7 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config } // Iterate the keys to find the newest, unexpired one. + isPQ := false candidateSubkey := -1 var maxTime time.Time var selectedSubkeySelfSig *packet.Signature @@ -223,10 +226,12 @@ func (e *Entity) signingKeyByIdUsage(now time.Time, id uint64, flags int, config (flags&packet.KeyFlagSign == 0 || isValidSigningKey(subkeySelfSig, subkey.PublicKey.PubKeyAlgo)) && checkKeyRequirements(subkey.PublicKey, config) == nil && (maxTime.IsZero() || subkeySelfSig.CreationTime.Unix() >= maxTime.Unix()) && - (id == 0 || subkey.PublicKey.KeyId == id) { + (id == 0 || subkey.PublicKey.KeyId == id) && + (!isPQ || subkey.IsPQ()) { candidateSubkey = idx maxTime = subkeySelfSig.CreationTime selectedSubkeySelfSig = subkeySelfSig + isPQ = subkey.IsPQ() } } @@ -609,6 +614,10 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo // Serialize writes the public part of the given Entity to w, including // signatures from other entities. No private key material will be output. func (e *Entity) Serialize(w io.Writer) error { + if e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC || + e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD { + return errors.InvalidArgumentError("Can't serialize symmetric primary key") + } if err := e.PrimaryKey.Serialize(w); err != nil { return err } @@ -629,6 +638,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 } @@ -787,3 +806,9 @@ func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgo signature.FlagsValid && (signature.FlagEncryptCommunications || signature.FlagEncryptStorage) } + +func isValidDecryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool { + return algo.CanEncrypt() && + signature.FlagsValid && + (signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage) +} diff --git a/openpgp/v2/keys_test.go b/openpgp/v2/keys_test.go index 0b276c23e..c9d277340 100644 --- a/openpgp/v2/keys_test.go +++ b/openpgp/v2/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/symmetric" ) var hashes = []crypto.Hash{ @@ -2022,3 +2023,222 @@ NciH07RTRuMS/aRhRg4OB8PQROmTnZ+iZS0= t.Fatal(err) } } + +func TestAddHMACSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + } + + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestSerializeSymmetricSubkeyError(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + + entity.PrimaryKey.PubKeyAlgo = 100 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } + + entity.PrimaryKey.PubKeyAlgo = 101 + err = entity.Serialize(w) + if err == nil { + t.Fatal(err) + } +} + +func TestAddAEADSubkey(t *testing.T) { + c := &packet.Config{ + RSABits: 512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + buf := bytes.NewBuffer(nil) + w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil) + if err := entity.SerializePrivate(w, nil); err != nil { + t.Errorf("failed to serialize entity: %s", err) + } + w.Close() + + key, err := ReadArmoredKeyRing(buf) + if err != nil { + t.Error("could not read keyring", err) + } + + parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey) + + generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey) + + if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key") + } + if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("parsed wrong key in public part") + } + if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) { + t.Error("generated Public and Private Key differ") + } + + if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) { + t.Error("parsed wrong hash seed") + } + + if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() { + t.Error("parsed wrong cipher id") + } + if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) { + t.Error("parsed wrong binding hash") + } +} + +func TestNoSymmetricKeySerialized(t *testing.T) { + aeadConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + } + hmacConfig := &packet.Config{ + RSABits: 512, + DefaultHash: crypto.SHA512, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultCipher: packet.CipherAES256, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(aeadConfig) + if err != nil { + t.Fatal(err) + } + err = entity.AddSigningSubkey(hmacConfig) + if err != nil { + t.Fatal(err) + } + + w := bytes.NewBuffer(nil) + entity.Serialize(w) + + firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key + i := bytes.Index(w.Bytes(), firstSymKey) + + secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key + k := bytes.Index(w.Bytes(), secondSymKey) + + if (i > 0) || (k > 0) { + t.Error("Private key was serialized with public") + } + + firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash + i = bytes.Index(w.Bytes(), firstBindingHash[:]) + + secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash + k = bytes.Index(w.Bytes(), secondBindingHash[:]) + if (i > 0) || (k > 0) { + t.Errorf("Symmetric public key metadata exported %d %d", i, k) + } + +} + +func TestSymmetricKeys(t *testing.T) { + data := `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ +n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef +D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO +8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF +ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT +X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1 +OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC +ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR +TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB +TxGVotQ4A/0u0VbOMEUfnrI8Fms= +=RdCW +-----END PGP PRIVATE KEY BLOCK----- +` + keys, err := ReadArmoredKeyRing(strings.NewReader(data)) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Errorf("Expected 1 symmetric key, got %d", len(keys)) + } + if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC { + t.Errorf("Expected HMAC primary key") + } + if len(keys[0].Subkeys) != 1 { + t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys)) + } + if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD { + t.Errorf("Expected AEAD subkey") + } +} diff --git a/openpgp/v2/keys_v6_test.go b/openpgp/v2/keys_v6_test.go index 66220a8ad..99540eb66 100644 --- a/openpgp/v2/keys_v6_test.go +++ b/openpgp/v2/keys_v6_test.go @@ -3,10 +3,12 @@ package v2 import ( "bytes" "crypto" + "fmt" "strings" "testing" "time" + "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" ) @@ -242,3 +244,29 @@ func TestReadLegacyECC(t *testing.T) { t.Fatal("should not be able to read v6 legacy ECC key") } } + +func TestGeneratePQCTestVector(t *testing.T) { + c := &packet.Config{ + V6Keys: true, + Algorithm: packet.PubKeyAlgoSlhdsaShake128s, + AEADConfig: &packet.AEADConfig{}, + DefaultCipher: packet.CipherAES256, + DefaultHash: crypto.SHA3_256, + } + e, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", c) + if err != nil { + t.Fatal(err) + } + + var armoredKey bytes.Buffer + armorer, _ := armor.Encode(&armoredKey, "PGP PRIVATE KEY BLOCK", nil) + e.SerializePrivateWithoutSigning(armorer, nil) + armorer.Close() + fmt.Println(armoredKey.String()) + + var armoredKeyPublic bytes.Buffer + armorer, _ = armor.Encode(&armoredKeyPublic, "PGP PUBLIC KEY BLOCK", nil) + e.Serialize(armorer) + armorer.Close() + fmt.Println(armoredKeyPublic.String()) +} diff --git a/openpgp/v2/pqc_test_vectors_test.go b/openpgp/v2/pqc_test_vectors_test.go new file mode 100644 index 000000000..dc10465e3 --- /dev/null +++ b/openpgp/v2/pqc_test_vectors_test.go @@ -0,0 +1,247 @@ +package v2 + +import ( + "bytes" + "crypto" + "fmt" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func dumpTestVector(_ *testing.T, filename, vector string) { + fmt.Printf("Artifact: %s\n%s\n\n", filename, vector) +} + +func serializePqSkVector(t *testing.T, filename string, entity *Entity, doChecksum bool) { + var serializedArmoredPrivate bytes.Buffer + serializedPrivate, err := armor.EncodeWithChecksumOption(&serializedArmoredPrivate, PrivateKeyType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = entity.SerializePrivate(serializedPrivate, nil); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPrivate.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredPrivate.String()) +} + +func serializePqPkVector(t *testing.T, filename string, entity *Entity, doChecksum bool) { + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = entity.Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredPublic.String()) +} + +func encryptPqcMessageVector(t *testing.T, filename string, entity *Entity, config *packet.Config, doChecksum bool) { + var serializedArmoredMessage bytes.Buffer + serializedMessage, err := armor.EncodeWithChecksumOption(&serializedArmoredMessage, MessageType, nil, doChecksum) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + w, err := Encrypt(serializedMessage, []*Entity{entity}, nil, []*Entity{entity}, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } + + const message = "Testing\n" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) + } + + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) + } + + err = serializedMessage.Close() + if err != nil { + t.Fatalf("Error closing armoring WriteCloser: %s", err) + } + + dumpTestVector(t, filename, serializedArmoredMessage.String()) +} + +func TestV6EddsaPqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoEd25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + kyberConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMlkem768X25519, + V6Keys: true, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + err = entity.AddEncryptionSubkey(kyberConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-eddsa-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-eddsa-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + configV2 := &packet.Config{ + DefaultCipher: packet.CipherAES256, + } + + encryptPqcMessageVector(t, "v6-eddsa-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6MlDsa65PqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMldsa65Ed25519, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-mldsa-65-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-mldsa-65-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-mldsa-65-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6MlDsa87PqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoMldsa87Ed448, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-mldsa-87-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-mldsa-87-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-mldsa-87-sample-message-v2.asc", entity, configV2, false) +} + +func TestV6SlhDsa128sPqKey(t *testing.T) { + eddsaConfig := &packet.Config{ + DefaultHash: crypto.SHA512, + Algorithm: packet.PubKeyAlgoSlhdsaShake128s, + V6Keys: true, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + Time: func() time.Time { + parsed, _ := time.Parse("2006-01-02", "2013-07-01") + return parsed + }, + } + + entity, err := NewEntity("PQC user", "Test Key", "pqc-test-key@example.com", eddsaConfig) + if err != nil { + t.Fatal(err) + } + + serializePqSkVector(t, "v6-slhdsa-128s-sample-sk.asc", entity, false) + serializePqPkVector(t, "v6-slhdsa-128s-sample-pk.asc", entity, false) + + fmt.Printf("Primary fingerprint: %x\n", entity.PrimaryKey.Fingerprint) + for i, subkey := range entity.Subkeys { + fmt.Printf("Sub-key %d fingerprint: %x\n", i, subkey.PublicKey.Fingerprint) + } + + var configV2 = &packet.Config{ + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADModeOCB, + }, + } + + encryptPqcMessageVector(t, "v6-slhdsa-128s-sample-message-v2.asc", entity, configV2, false) +} diff --git a/openpgp/v2/read.go b/openpgp/v2/read.go index 5ab9aff53..fd4b986c0 100644 --- a/openpgp/v2/read.go +++ b/openpgp/v2/read.go @@ -26,6 +26,9 @@ import ( // SignatureType is the armor type for a PGP signature. var SignatureType = "PGP SIGNATURE" +// MessageType is the armor type for a PGP message. +var MessageType = "PGP MESSAGE" + // readArmored reads an armored block with the given type. func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) { block, err := armor.Decode(r) @@ -136,9 +139,9 @@ ParsePackets: // This packet contains the decryption key encrypted to a public key. md.EncryptedToKeyIds = append(md.EncryptedToKeyIds, p.KeyId) switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, - packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, - packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD, packet.PubKeyAlgoMlkem768X25519, + packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/openpgp/v2/read_test.go b/openpgp/v2/read_test.go index 4de095f01..acc26cd58 100644 --- a/openpgp/v2/read_test.go +++ b/openpgp/v2/read_test.go @@ -13,8 +13,10 @@ import ( "io/ioutil" "math/bits" "os" + "strconv" "strings" "testing" + "time" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" @@ -29,6 +31,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 { @@ -1061,3 +1070,105 @@ func TestReadMessageWithSignOnly(t *testing.T) { t.Fatal("Should not decrypt") } } + +var pqcDraftVectors = map[string]struct { + armoredPrivateKey string + armoredPublicKey string + fingerprints []string + armoredMessages []string +}{ + // TODO: Update with fresh test vectors + /* + "v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateTestVector, + v6Ed25519Mlkem768X25519PublicTestVector, + []string{"52343242345254050219ceff286e9c8e479ec88757f95354388984a02d7d0b59", "263e34b69938e753dc67ca8ee37652795135e0e16e48887103c11d7307df40ed"}, + []string{v6Ed25519Mlkem768X25519PrivateMessageTestVector}, + },*/ +} + +func TestPqcDraftVectors(t *testing.T) { + for name, test := range pqcDraftVectors { + t.Run(name, func(t *testing.T) { + secretKey, err := ReadArmoredKeyRing(strings.NewReader(test.armoredPrivateKey)) + if err != nil { + t.Error(err) + return + } + + if len(secretKey) != 1 { + t.Errorf("Expected 1 entity, found %d", len(secretKey)) + } + + if len(secretKey[0].Subkeys) != len(test.fingerprints)-1 { + t.Errorf("Expected %d subkey, found %d", len(test.fingerprints)-1, len(secretKey[0].Subkeys)) + } + + if hex.EncodeToString(secretKey[0].PrimaryKey.Fingerprint) != test.fingerprints[0] { + t.Errorf("Expected primary fingerprint %s, got %x", test.fingerprints[0], secretKey[0].PrimaryKey.Fingerprint) + } + + for i, subkey := range secretKey[0].Subkeys { + if hex.EncodeToString(subkey.PublicKey.Fingerprint) != test.fingerprints[i+1] { + t.Errorf("Expected subkey %d fingerprint %s, got %x", i, test.fingerprints[i+1], subkey.PublicKey.Fingerprint) + } + } + + var serializedArmoredPublic bytes.Buffer + serializedPublic, err := armor.EncodeWithChecksumOption(&serializedArmoredPublic, PublicKeyType, nil, false) + if err != nil { + t.Fatalf("Failed to init armoring: %s", err) + } + + if err = secretKey[0].Serialize(serializedPublic); err != nil { + t.Fatalf("Failed to serialize entity: %s", err) + } + + if err := serializedPublic.Close(); err != nil { + t.Fatalf("Failed to close armoring: %s", err) + } + + if serializedArmoredPublic.String() != test.armoredPublicKey { + t.Error("Wrong serialized public key") + } + + for i, armoredMessage := range test.armoredMessages { + t.Run("Decrypt_message_"+strconv.Itoa(i), func(t *testing.T) { + msgReader, err := armor.Decode(strings.NewReader(armoredMessage)) + if err != nil { + t.Error(err) + return + } + + md, err := ReadMessage(msgReader.Body, secretKey, nil, nil) + if err != nil { + t.Fatalf("Error in reading message: %s", err) + return + } + contents, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error in decrypting message: %s", err) + return + } + + if string(contents) != "Testing\n" { + t.Fatalf("Decrypted message is wrong: %s", contents) + } + }) + } + }) + } +} + +func TestPqcDraftKey(t *testing.T) { + t.Skip("skipping") + secretKey, err := ReadArmoredKeyRing(strings.NewReader(v6SlhDsaMlkem768PrivateTestVector)) + if err != nil { + t.Error(err) + return + } + _, ok := secretKey[0].EncryptionKey(time.Unix(1737373639, 0), nil) + if !ok { + t.Fatal("Failed to verify key") + } +} diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 9322b949f..2f09991db 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -788,3 +788,555 @@ U9I6AUkZWdcsueib9ghKDDy+HbUbf2kCJWUnuyeOCKqQifDb8bsLmdQY4Wb6 EBeLgD8oZHVsH3NLjPakPw== =STqy -----END PGP MESSAGE-----` + +// A key that contains a persistent AEAD subkey +const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv +/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k +zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ +AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA +/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX +mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy +RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73 +3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW +CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l +PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv +jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD +=8TxH +-----END PGP PRIVATE KEY BLOCK----- +` + +// PQC keys and messages +const v6Ed25519Mlkem768X25519PrivateHex = "c54b0651d0c6801b00000020d21828c743986e8d46fb231131bb74a639f18bbf78b7c4920a98f769cde8018600c152009cdc6ea46cb0fb1f8cfc7a3f969ecc72f7667b76057730c9af31cb7141c2af061f1b0a00000040050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b03021e09030b090703150a08021600052709020702000000007fc3209abba0ed0a5ceae3c8313381623a8521df455d176e80fa958c2068c1a3bd3340ab45fcbecdd6d0d65a31838f401bf1ff4d4edfb5d09740047584164f2e61b1398835dfe2ba3feec2039d4eae8d295a9e1dc06200a60d34344add709d9a90fc07cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec29b06131b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021901000000009ca62025793b46d9634a942789d29c10758f74e133751ed7c0703f4a1e364e0e9ade980cfeac0ab622601200df9671f06153b6ca6100c16b0441c3c599c0793d4e69a7e5c365d6b09d161b0d9f3cc0e4f1df99d7d6cd5f5673fefeca6c3879f07ef604c7cd8b0651d0c68069000004c069b1ae100447a5eab36623e9105ae3e4d76a7ba2202116b2b0198fd3840a266ac926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b23209300e73edaeffca21778477515e0fe65acb4fa795fd53bb481ac7c55df8e8f21606e7a856a5f080271c27a689104be69ca36d078b3e8c5463a743f148e13021b0a19b415c20ad7d4444360cb9a085209fa3a6862861771428971a4b8b3a108d595ed89791c68c7c2183ab6a0ce68c239ad95b922248bb20b0dd3ac6c6b2c987b9b317789cde025443531c9d64a0de6790598a202e5356682455ebb4829550a811a5c69b5b690b4d1a1ac3984757938828a69cd317f3a389899496646bb1f8ab480e2f77f6388221a4a575c3a7781f5c88325bbf773927b892fcab9b16e6386346620509a97386c739fbac7eac4c90053b9ce769a8ae6774b71b38b1081235445e4c0939e536e5f86c6833853891abd345357f282693498a1bd492fc11a64f4bbbe4d56bdf7f353b252c7eb3aa090a70a1d61897baace7c441e84a862669124b46000e491b3a5f0a64798ac46420982ec6f7a958bb221270d1cb977f0137f9b406775ccbc475f334415f1822e180b5478211bd7377b9a45c555460551b61884b4c2e2c558ed88351d618a01e30287677613a35b5a9434f2a83ab5a0bcddfca6a0a8af27393d2873ab20e55339c7c762c29fd366061b5b06b69cc4786494d44039e17b5d67e30bae15054371ae4e03c2eb2123466c00ea8bb8400c2bbb82aaa1826c39676976da9930244c7077ac5fa4468933c587065967870c234754efb59a81eb5fcada99efc359fc919ef6666c186330e41719c5c39965b19a1cd71f64f0529ac39ad7c43bf2c7cf9196cd0907522b2369cbb9af7e7b1efa6803177952a7386f88637fd55909fe0a4a89e5c96bd5616d32b140d6ce2bf2a800332a4161260c837f7b5c0422cb1de53cacb412c23674596ffc53b02747c259b992d59c29ec600c2c6775008240f0af26a66ab30ca2c813676aacba0226392f649209ca276705436ddb51b893586bc80c1f276fdeec02564a3f3c7bb250fc6eec921b532cb8d1a29673606e4e089f246bad5735642543b547b1308df4afc9bc41739a592a11a1ada49d74fa745bc3015306c69d0c00a7e3508ae751fff0b32d190d893ba3ccb05315fab3bf268e78e7cee7c807d52c1e016ba9e5eb2ddb374b92bc90e32450fb697a6ac3c6e480650aa360b8b461375058f4f92c5b006f0f3c7b969080522a043b491ef26c109774bd3cf604f938caf0c62a0f906b56d9cd5daa413a5bbf0bc23b4ec0c09e0c6df2ba5aa12544598ac5514531696c1c9832c0071b4d8b817305c00e113221ffe3c24e670ae84ba1cbe11023cc3dd796993cfcc1db80189bc28269b13e50bbc44fbc5e521a4f7d378124a072cee0521236b445f40915d5165f7323a3546c8777702b991951ebc5ce55958c7a9622e059b6c143f8fc29a462c27af24c59473ae067491ff953f2944688a0194c0919d87902bf750d7d406890cc91f8696009d2ae0f3a87732a167cf68d3f715a26e83ebdf738050088242b081a61adc141b0a357a1453aa1c607250b70977b9c2f3eea30c372b0f3594efc899648494794797c96e92a9beb7b89c52c4052c7b6722b521616813742d730996884a0d0eb6a32e12c335202ac8c7618da4e6df0a8b6eb13cd7c19efa305af595fd03b257c075e4a423c3e2107b1c62d4405a1ca30bb754668a4f8be9b8caefa427ed1341dc926b53bb67ebb11bbc38669295079cae8c431c8327f27289ab1ce277c0909c538532c0971e23b8535753a645fade64a5bc9122a34108b72b30e653f2a5a404306c3e78291fe8506e5010dfef87bb425060333a8220914ecdbad4b2a02bbd31f9568aa362247c5f882a846a9c9da3691c33dfd7935fd46ae5e1b1e7333cf2f1171887b4a76392d74190ec8603e2a0071d01203f210aed366a6e5b2c4d8b5036214a863e2c835587377d919e216b9c87b1ef0c201ddb22d9c1c1477ba9c04c356fcb057db682be75810fe60b03163c864a970f302ca0898c9cac632a879526696272dc60474f80dd0b71452e35e6649744c4554d11b11382b9fc1416d21972117182ac7266b7dec7fed7a7c7844a89471b3f1a39481c773f037755ad9aad7b08643348be71a026b89aada6672b464cda50835a064bdcdc0868b1109842910f915bde79143502023dd7732e647841aa6c841002e8394c871f9b9f7f6acb690c0cba325dcb8a15566948eb108cb35012ec250ac118e32d9a3aa404554d92060d567612803e4817fec03adeb884c525a964b98bccee4678994297ae8b836925b1971999864995693ac3bab16f5cc9aebb58957b7528e185bedf1923286260b1504e780b4d99a74e84075f8469a81b96fc4d393ca080b78278429e51c0361532d89cad9381faeb30dca3878c4ec65ed93b630530c67254e4e8a8966dc456f316651e1b4a1d03f12cc474c000d2cb7a67b911489057f3730c703458312e429f9698f9035bf7c6b5359740349c6234505694deb93f3b6a95eacb7a64a5f26c04433ca29ad1003452b26b52994606b5f500945a7f81376c418b2137c3023b3de0741860007cec70b2c3a7b370a80a99596bb38c991129acb332bf0bc92e9bb5d9bfc1f2bdb4d67d28b110a6157dc51ffbb25920038f2cb925a300b3720ba38c19871db5901b70f2bc91849252d26d486a7555739ca00966c1f497c2942207b17a93f6546c6875971205c13ef887ce78081c5250994785b33d259d53876c5d9332a10b7f0617912496b4afc0ff0e9c95266095b62906bdc06b231a666f47071026aca24385e14368c5bc06d77c19c59063380b16f7753a8e777e193401ddc099a273d99b33a14a2adea78baba10a8375c920a356cbeb58ff552615ce88c316716e4486a9de4a203ba9d04b27131e1b3feb4a8e462917808725869b0010a0e2a39b855aa4b8d979e3db580880b344887c4ec20401dd29ab91bb904628b4abb605898953ab4a1aae001453005811c4811e6119fd8a2feb904280288c93c0e8e7b07774557c6592151ebb824358d5b778981e24323f8257f891deb85afc1bb35df434a1e1b276de33fcb42c237075ba26a7bf650561e27cee655a327b0147d24577a98aaf4485639e0c7217b218dab09ad57cbe679b48e472323bbcce4105c9927a324111b32c6c5cc822149d9c41b593d9a8c61c9034029a6b9ec50c8ca39c86726aa62e8498cfa86a40400ac094e6b6422463b53b00a0af6eac0871b338dcbabc60b0575ac06b1786285ac78f52830e40b12ba53cdfcb6a1c5b68e6ea01e85d3115c09a8734c68aa06c46b3c742af8239948cf83b6709b079b9c750678459b6475706a701b8c551895851564c8c66a14c6a17c6d42eb7231489c4fdd38dff516396b232093a349cfb4aabf9beb989f38a30b764d31f6d8e8299c004631764f1255d6e70eca7c602ad2068d4c545e60ac8b205ed85b38571d1a2e7491a8957a7093cd14ef24c29b06181b0a0000002c050251d0c68022a1068b37ab96122997c0116b4003d3f9279048a6ec4a0e34e12672552a9c9854c8e4021b0c00000000127a2079d49c8346bb12ceec093d0d97e8a10d2cdfd387d3676022919400b74ee8704b4ee55a650bd399a91c76c9c2a016e84cfa1956649b0ff38c72e94886e3f2e54394d7f78320852be956d9123983375970efb57e91dd42dd550b9933552101d70b" + +// const mldsa65Ed25519Mlkem768X25519PrivateHex = "c5d6eb0651d0c6806b000007c0e689bac827d939ea2dc85841e4de48c5b0f109063f51835d2f8b6d0981824f768668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c25443dbfbdb7db4265438dfe815975ce2cc05e0f04a3cf805bdeace2d343e9df219acf916efc76c00174748f69ca4e0c4aa1ebbaf2f98a951f2988386234874df267db2dadd63679fbbfffbc5086440144fa4f8c24123bd89a8c09dae1f39c23a3e341aaa42fe2c8d7cb334dcaf5d1cca94d91e9c57e87b7e3ed21b0a7da2737372c3dd5f6fc538fb541c9d3b3d5b0b0b6999156f00fa6f42192d4f3693c0db26f26cc1830525a3998471ff8634cfa4be35f15fb8b62a7b3a92ae41232ad4258677fafa9a15c9953c5a0da1f3bc18afed68b802b29aabece749cf77a37e3ff6a65d2a2f067edb886558394340615601c6d69ad1adb445ac2b79d12432e7bb9e51d8ebe25fe4860e3b60cff5985f2ee7f7443a60131923f31e5bfd64f3026fe25dda3e17d0aa80831ce7c5ca0c8afc6fecc81b37eb8df6a01a5adedb35b94b7acee1c4dd5486148743aa7bff984a7bc295e85ee917f047c919ce2bb0d74f7ebb838c634c6d295c0283ecf29873d81be0b2fdb2011f338e404c61a51d8af2545f0855e51e57a948e40f7c10aafef8d9bcba627a28daf1792954fd90d1bb4fc90ec649614d0b99e21b736453c824f5fab7e8fd23c903dd31bc5c6f1bd1e0bd98738b3e3628d1ae26dd1fd3a9aea641d96820d3ddd2d907e35ff5a14c52a8dcf91d6781116015acf446076c7a93fd021283715e8ba7fe65f2a8fa875821c02a9e7f78f8c0478eb1923b1efe92c9d100e5ad6afecfffa89e542c31d8dd5c3f27e71936cae1078c2d626bd1acc6294a6ed03904f6c01d3d25d43bcea8b84b307ed46fb9eb0002d38286e5c07815409e7cbaa32da49b1abc5434e5fd35d75a12d62df349755b7a2be1f5026c62fcdb0130d086af95bf67616b080ae4149fea634c3df0c518b520a8afd5662f72673f15ecb1ffca52acc6661582124755cd7554ad24044c7227e2b96b5e2ecee96dc0f20ad63636ce04cb36d44b39e245553751efdbf84a151213c208725e4cd1348c9467d7552effb516fa7e56ce258be6da3f9ab9788c96d9186689b65c37c9dec7c4f90cea5532afe6de3a32ecc01a9c67ecdd691cdf2e7e9db1a49a2cf4ebae4bf0d8404a69a2ef9fcdb916b7ca32d274e911ac5d27a63bb8abb882aca3327db5cb0e053709d8936592ebff321621e96917911a32147b420da6df5d3fa9bcca8bb8e33b35353980cf9008a452399131b5bc4fa3b689e5966cfc8b047cb237e7bb3d7001de82adaf9bd0e3c52e9192b88f9233a83ce2899ef89339acea833df44aad3b49723d8d5e1b15c8202e3a2ca8745179a8ecb4a2dae80809091e4cc95bb14e9af0d58fbd769dd4bfb5f9379ec01bedd44e7219dab0a099efff64daa4cfc20972b8a77293f474fc69c5a4589e907d8e757588de054fefea2fda553e4672e2a6173f880ca4983547481ea29afe09597fd3ba094b844e725053f4e463c10e81f62a3ef072ff829da828bf4ca95305334571b5879666368506c8a6d609faf01c8d2322449c147b6f289bfec8c2af98cf20658acb8c28e33b1dfff50f1bcc29d850f20d0cf85a34e5d83907b2d87803f83bff3b255410fb557374d188d93de3f50fd239070d200157145bbbaf313d4799f50256e565748bad9edbfbf87bd116433b63e04cdc8afa7f79a76a79068523fab225702f6a6324cd960da6eb4445c2272d0d07aef6edb0ad2432372c8c25d7b48ce3f7b44676b04d5144ffce20d6ce29637a9ceda54211d806b1be7b8199fe5c0ec3e1eac109e0af1d1b8554a27c57655975e8679f1c8938d4444be05a93ee21f6ac6d5beed004ff062ef0041d5af76e683f4b7709a5ec859392cbb0889e646cec80fd1c112271617a0e54873193030b99d782297638e42588f025691fb5e76c959ff01b8d5f7c55b88b5ba239f121a17f02699617d1b52391e179aae8dc53a15a864318abb7f832289e9a1744c0eec3b5713cb62014babbe9a19d132115ec881fb4f3aef20c347376081873f138102def6bc3681feba07e99b4d0f759e98598b335e132e77940ad871d62c9b7b358218783ad82352fc33c92adec762ef79de8aa310ac5efaab7e39c8af61046349e61cbb73b66fb9fa31d2cd92f48ab9576ae77abd902c7a34cfaab82eace65bcc09cf0b413ac217215bef16f5995cd11a30f3711864b6675ecd694b78e8038b6d46bf94a1f49e33d9d3730ca76fcae8a113ad5ab168f6d0b3d66b40529fff69fe0e9429a64082c5ee0f09a543836cdbbf36530a0d5de3c233d577a424df006f62939ab9306dd5b69cfdd1d4ae068941ed9d13c89cc08c12dd1f97e3476c6017c7376a4a54c62d8a0b4979b6314fe7d246eef1d9644ce43fa1abe4c7837a201e9cfe039b6ad68cde19a4a6414475b0d7bf4a7e5a29b73cb10a0b2fbea04209dd421825115c6937057e883933629588d73598f2e21d1d3b82cf827d947bb4a6459e1de5b35159ebedd0f175497f7d8ede78c33224b122084d774ed4d901fd6f0a4db1c506a371976f3b9be7f298f160c61f52790838ea7b287730506de6e845964bc9a57ca193884efcf6338e1e919fe6cf50ab64be3892939113f49b75e3f5787cee211c66b5701c81f1aae21914974f591ec3f5fce90197b9a99e539540378c43f483b622a7df14bfb1e78fc2477ec665cc77846270f071cd238927f30853b3bdd81af62966737bb3330dd42920f25df937197fa63787bff7008a5af22081c6d776432b9a337db6e2b9d48e852b977de119f2a1e7e206ac44c78668db2bdd67363a63f8cc7fa40939654bcdd0ecb2bc8db20221a72e4b0c2544998a805022b6e184ef7316e79c3cc81fc200df98baf393880376d3cfc8154ba4164ce26af2b4c88d481b46b8a1116746a23275b86d6a792a8de213b1f3168162c113d67d6c79da2dee51dfbf127db85144982427d96ccc5358dbdc996b24155f724716245465856830518072684535417785818780626831538346245818610084785041058182601151184448540475448854025762762835832218363636004507380162577778167145133565235852726284544527566462850012530407802500056423858701586866543075633565875138036160685133070205260506154074788810800287271565047075210135826605370103543371255180663882315002085718048746135321452127576667301648280815534732250311180251127854856732202288465386017785441700585162617825151361025662613185326277677872328655732033184723345802426113754361857103080521218426657444166158020301135036724566684757402780203151150134816423334652428668703406606283147174553235124043607682586560172614240026463371718355855002411723770734030431212041660767825240443065557038128218322424008352484311517554351711364847014512580778578733116885156882347544581070012043474778481467818416767736242006703106471735720343244444778430712655488408726336232115411047616612105371727624184613128456085364876736038515616147018812384114560133441132234311473766776068106381840071411637680446524074765012440117105043746850126623578222115883553025043583511858657434436208831724131846356634537862140361534830430755015830544425071744270847780781875014141312307326373215116521750500001562181357170475865803250812878088418840231301724774388267568566050344277151274515840351667153470314153720002781211118053366155154642766453433386486208877642743213845027381300107736186201741503844482248275008565502176848662122251651177560248453426314243260262287832680575024638456813147447176451188340420424874876380668226524743001621124487080800414845825626475185816266852618425823224588240232514447416230035304436854770167805740276502515743251274324015503854141618065761443566821184245270888450800715237216554521152827472602337886785102474662701031073206732110026825147364884417068010334565074612131254378423042306878525132514865325382515054184733123701274474688620637264814423132702720823171444338685072440406620017838053248084765010144363673731313127533488006870511220876820848184762087784623401060338067118535106414778554856680078437737041338337052455205853181753708157405016044483507343602082378506283818668170784827516441762687605246358287851612672035318220508307454758464330452212820383674750212551374633034547004457882427176820411164742626503314111640247738030763285728670663710188211474653260562348212628355065178076358218175427371366502723033671154822213378017160684126156820002528175187667804760068711500611461170558445537206510546751406610550585824750432177880836037275460218437554640254135346047277145262863446606161820141116006641354205861670725451410452176582537243648776610708666133217138317682702833046116817434517506818565678065727733710305523506625833578275624545006371184567381076655484627641117266175787543386077150357636163354547064830548243058746374747062606587100250271015544218230087560333bdd8e3b2311d064b472cadb295d4cd3807def18d3419493594eb41691ba2f6fc45c91f5ea1a370cda5c7d7c86f593155f3a7b961edc6a5dac93002f5d9960ecba3ff8476763cb9a40bded8477dc08cb05e580b37d1ea93ac1a38f6669ce85f0fc8de24812ee0de32b2da3e990e458855dc501bf77e7695406520b9d4d10c9af020162d06586b2c1cb78f4064894cd8d0d2dc602e15fc50c48b6c7a65145db49bc6182f5e0a83081ed6072302737bc7b3a1ee15b4af2ac62b6aac172d523765fe5d2a2326e1bcd03995c0dfaf3835fa29492ee53203dc682cf25128848de6351b90c59ad42ea3eae2fbf7f59d8f463e7f9f18acd7b80d734830540fa14c957dddc2d338dd218c4ae322680efb2ed5f6a72eb26a074b0eb28daa0c17489b029b9f95ab7ea5ccdc42a1ae1c868ea24deb38543095473f89e8484ac68b0adc801e6b297434bb058cf9d5b195256d58efe18bcb54e5a45ec59d2c658b92d8a005f67aaae97a22f51dcb9f0b7aed4b55feebfba37008f84c367bd374de3abbcd07ead0bf010f8236b298bbd9a9fc0ca268068d79b487cfab08f57ff362e997af288a5f604724d3440342dd994efe9497f09a666cfaa12c6eb0828c4388ae40d45df5e5e76c9ddc9dc2fc9be1da8581b7b93dca8058c90ede64aeb8c81431cba9222942d6440039d116992b2711f1c8f453a197d7bcb999abb1588f8fd11863282ff6311959b1b98be9d6a09d696ab3a8397fa45b751a16f664275c90dcf51b56f26e6a2181cadf1b8baf027672bb92126f16caf48e2592422f169951b1e3e05ecc1a6e1851c1eb307c02f24a372596f28708e5e76223e4af41d89d193335abea65372f2414b1c6b56a6efc7b61d58cf3b2d1ded96761f214b22ebcda29678042ec00078a7f0a7ffe2ed4e31d083b5176045e09223d6ba84eb7cc51ae5b76aa6b8de3d86f745fb6667bdff653f196314553364b2f0d74e3dddd36755ee6d53a387354579c47ad9110161a174dbd993a46c05cd83d69b36cab71380911b8d22597b5f6938648c28922326ce2f0293c1dd1c5979673ab8eb3bde840f3f4aa65b975f7eaa5a6765295e6330e9c64ddf82d90b6004c39ae2376fcd288481c1cc601a56daf686868478fe6dee4950d5649993cb53777e2fb9c4bf37dca74a85c952e1254969d0aea98f1fe53daaee52c420329e27cfa3d7d30ffaeda58e204f0aa169f7f4f51286e88bfbd4f1a34f5ac501a5f7d1a305d417ed2036410d5425806d366ba7e75725db2081565a3507fe343497d04a270552d119db411e751fb11031ca260cc35b1147a1f018984532ed7aa116737a49094e35f9bd65e4a5602a25dc50abd9576a89af58f62a941a463aa0172b9fccad5e36a11febbc365b5e09c177f8b175c1fbc7830fe7f054ee914156bec791ced94075622df33846b71c42a20d83e0d16a94f1305cf410ef5ddeccad22fd28e19571d5878baed4a1aac38b31f6aa50881bb232dd690661e98df34e8c0ee9593631df9247a26ea8bc7cd75b743ed8b636ce3705ca729153084397c70bd938c10f3f5bc8d65d7da387428292da500b163143842dd698ae6ae32e86c24a59ec1293ae785cc2b14daec651e9c4f85f75517a0572a676cb92c86079ec06497a39288a14be9892a8c34797d41a95d8499f9bd6654171e40b4621b646e1b5e2e4932e8e95f1f0166ae8fc06360980b15aa260f307d4286e74e49f952dc886e98074c70c9513423dafa0068779145da04b1adcc70bec232d83f519a10e635a630d10a7e015cd88d09acb7e356465c3603dbc584ed9d595aaecc2018b0b7facd217c52fc02759ff584f5cece23c5e55c8bbcc68883a68ae1ac4cc4dd177018b4e6b8b4402daea4ead06901f68596ec4df3d845b488e1729eaa17d566392fd6597b14aac177b920dc1c8e75ff3439facbe29b3edbc02c5215c3083feb60acbecdc0b0a2998127a6776eca2d1920ab4e021cee82b1969b3a2a5e5336785c993096b0b480075a2b5bf7a1fed06043bfa8d81d47f8dcd0c9fd585a2a432f301a628a59dfa463c655bfb95358394294c0dbf9ae77f91b37377ec25392ecb4b262dcc0efd62774c5f8042616565eca14efb8b5197e30986b633c58cc0d64c5ff4ac19838873a20a3f412abc41a905c9d7b278bb603be49fa161f4cf5fa06e25949484ada45ad03ed4d85ece55cec6b12e57abe10a328a320d273d8081f5a8124eaa324cbc2af6473e0bec295cdce96119f5d08cfcc36e5719128282a5c968a0a8446a4175f86b3a43b2e39f95b578d056ca31760ee9d75693f4da933e14cda592b441c43a3ee68bd13bf0f8fd14f92b95c4f156791c6b23c1fe1526dc677b6be2a1f13f3599dda953291ea6f82cc43600988a5e8379be494397fbbb00c1bdbfa2dd521b477d641e674ded2e5b00b13f36279997566ba768c6a1a42a79212debae944ee54be02d06977bb08fee99e7b8f374f923deecdf1a528d59bf75add5e1334f1dfcb0de5febc3c24ee135c42e39d6f0c3a540735a393a643b41d774d954472ab15878efe66801c221e8be46dfb5964bb23c912ba68296bb600897d4cc49b0424652fd03f4d0b5f391b34b9a08d1ee644a6a72b524de7354e0eba28dc8a80c80f87c5c994fcbd846e3a5b9c16f49720ac1b1ad0c91749bdf2a96ed8f13b7c8cbad2501347ef0a7fc8ee9c73ced362007b76490102d511edf638422d5ea47d7bf659d09cd6e381df88acddce5d554c962b6b884c65728e1654062364c5d6aac763cb2754f456692d6f651af0ffbc5ce34a5c49d93298fdbfca5ab41205da7ae93c28d1d97a31265b77981924ddeb44082905f4da1d3489d63e8bd46c73a3c5f3d11e2078287e3c5ab07cee1e977ec8130dadbdfbf456ed308f0284c2c1962317e5def7083bf19f53ace298288bc19b2d00e447e5c8806af9b818bfe577a5e1409e4d04c4999623c1c3c81f1b4d359b75a26cf42f86d8ecfa76fff08d89b3d341cc04dfa65eddf67fac7eebf2bc6b5ed64b3e3c3cefc18e4e5a84c012996888ee759c93a1c8b250b7f50953b5546826b65ac85f03391eb90f34c568232a1d59f5872d0d24c649ee72cbe5d86af8dcc512a7b2bfb9ccd8a670b23387fa929a713298e5c87f66c703e57d68f7c2878fd752e99f0f94785ac06551bbde9ef93a717328fb73d468852edbe411c6415be59afe1883cbc0c3f3ea15ba2ba65cc1f8a1b4d835bba79994b83596844d405bf10c4ae3caf3e0bf6edf12a08a0f6bd112229b31ebe3b30f9fb16a83947358bcf5be6fcd0f95cbe97550f185be720347bd469bd5e38ef561dba1c4fdc45acb121528eed02cd84613c529cab2c8e44864d7efa47f4f4790f0007c6cad427ccd77b1ce96436832a51bfe640330990239603eb94a20de889daf22d2ac1b18cdf24ad27c20008b2979ba8c400040b18a35229f2f24d38815fc88ebdc1169432d54a5a394c437b8d1105713eddfaad245d4e95a42710b83ab451d4bd2842908897c19a8034a7207c15f212ceaec2ccdc061f6b0a00000040050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b03021e09030b090703150a080216000527090207020000000091c6203727303510cfe030465707c081ac03c7992494bb1f0bdbb0abd0fed4dcaf7c8bffbd8efb86003c970de63afbc24fac031a7830c3c15d6136aa81389d0aff1a753094bda4cc08e9ee5c64eae9b7e780f989297c1fd20ccc94f80c2733a0e5900a794adebe7f277ba92a1af064f692974b917523a6e7db2f92d323785922a6964d78ec037240a71cd87c7fd9dddd18754784c9e976fd0919daa51f0d46ecdeac883ea54c30ed6f00b3835d0db60f615b777d85d33feefcb82939e3444d8b7b5ca0c92514e10def322eeb09ce3a5ab28efb1dc08681dc0d3cb23dc54d2e34a11bf0740f20a528dbfaddf1be9c3ca4b352b15cf35438f195acf7b6976ce75b550c9548252058a19e134ba84e619045b4809cb182c5cdb91c067ed80b673834dce25412552d1675c75e7adadff6e0130ddf9c95c66c2e256e23d85e7e7e7340eeef6d2637126e985f51d9840ca907642d598c3c0ee1b547752c8715a55d86e558ffb4ae249cb57882d799064f60821812265fec878ea344ebe6b3db1f6a6380af32df2e46b59a162decafbabca0bb50c88fc3e60ddcea991d6b00f7feb565a016512220c4e71aa51515d3b59e3ebb5be6b1f48414d26c413730c4b66b87aff48e8ac7a84a51368831cc92434fee68698222304df93e49e965c422ccb6071f6c633580b0e1df030947b5c340a1ca7478eda35abdddcf633d342a4a1849f1473f6b202fd06e0f79914a6e0ec42f240f3ffd31b4db72ae8ff99922046493a8a688048f0b4420acf875a4fcd1452f62645f9186d87742537d50bf2d879bd62207d32b2dca5cf95c86b2314c50f44ba3cef3daaf29b5ba2b4dc2419a18748681b001c2c0567bd4d7ecd69abac6b8e1afca83b09a74b950fc7f12a2ac2a4bd7ceb6907db35442fa69c76f3a961a563d7ee3f5fcc7ba8d3e4f492225e047474e614652f672e696ef69afaf21d55a8ed18df029c282ed3b28e3b75b6e3a84dd059548dcec73eda4beb17f5557ebc0f816abc1b1e111e7a62273d984d090033b0f1c6dcabfcfa691f0d76a506b83dad6ecbbc72a9f9c623200f3247249070e1ae535b87c57cde7a20286af09e06a1b7b7800522c82ccf2ae17d9559c60b93fab493c8632370de4a07a38ef4cd98a43dd1476a09f45fba12f58e8f7f130d314de439f0b7e55f5609a056c94f35b8bd567b88a2ef953e5b775f49ca5dd665441a9598ee303b037047f11282fdd54cf1c63b748b557e40c2a7eafa7bd6e66790f366baeb2dd127b9633f3cc923a5d0a979f71e44aa06b4330d22ed5bd0c97eb02fbf38157290518560ad37d0e4b7083b64b3519b02c90c4697adc506dff57ab8a2b167fc1789fbd9f4046d5936f5b3a342c4f16a540b76d7d7dd4ba59fd39adedcc1364b6feb47a3a652bba94f26b3a997095b6f4594506fb8f2d464d1577d0e61924ad637c30e996cb6dd097290504a62cb328db85c81c064f75c9f445f3f9ea992183daf4991e59ca45b781a46b0ea41487b3e85288ce64d1c49af99ed5d531ce653b5384c06714a7efb39bb2b32756e786b455bc67c8aadf6e28f9f39954640695014c207dd3e2ffc3b6cca7600a31ae0f499d8bbb267451703885a51ba8b2f792a05a5dfa0771d322d24477e4a3c10c6a5ce5d835d35990bb6d3593ff9d4a24f4bac016de565e92084a7c55fb80a18723f902854de327c93088a65312ddf8dbf2fdcab60e0225943f4512905f2056d35a368db797dcc607f4c46a0606053b58734843680f1b23f8ce05cf98ec3a3568ba8afb9a1bb713a52b826efdc05726de1555dedf4a1e5ec16e6e1dd9c65280d8163db45de77eb2729b30e71ce4298dab5eec740c194bcaa79108048b21575fd0be7078e1a0b461312cfded2fdb0ba2112282155eea7a5e483668e67bcf6efe48ce4582965ae5513cf9a6532c642726a441ac4a41137f3a62f7e09ee61c652086688dcc6e0734a6edfddeb7e28c1468029d1fe92396b70f2749d340896dc0b83ac8ebe44b648317cdca7bea625450d400a785b4c510720ab56a967fe7d014985503d8dca8bf64414c9fe00dd1f1efe84eeec476d4dd49121719ca57e7e08dc4fc2e150acba2e1b91a86085fb0d21f2795010b11cb8c06f4921f407dd799358422a0feac2c363c6f88a51d76409d85d695a171de0c351a193ed30f6f72d91f59b8a52ae9ea8cc3991c3854a0460931e138138f3fbad63c045fbf598cdd0a1e6235ad076ac6070816e5b6143d1c92250f91666abb6d46f60ee8c0e263a79a51128c8f50ccd9e2f6b7d15ba99330b4665e62ad1dc8afe9cecf1339141135794d107db326411fbec43a0da34c8c81f6a793fe861df8a2f24dca0740758b5f0511e0008422f6fd407f531d6620723f287a8d4e63aaa0d57260193af2b4d6c7929c638d71f8c5e30cf46c278c1ccd1c32f488090e91dce1641edde1b8e872990c27a518bd3beaae98e513b9b6906539a5175c003746498b2234a2bdbd33f8342a808d934cd2f4a63e5ef8e98dc3ab7e98032279507a5bd9a859ddb1ddf58365e8a88737558d2db52a7da0d8f84d85496195af8431b4451c704812f2ffeb0ff193109e7ffac16ae067c7609d38e0eb78c12da94d40cf81405077833e9c260110e3deff88011cbfe260794cd8c0834f39ebc938bd92ef91236287a6ab38c25ad729153edd923bebdeacfcbfa5ff055f0b58120d398124468a35ce24e5bc85ea2722cf0e83953d8080eb89fee2ba87ee9d45c101da5b28b7a117f12969597dfe114dd759f39a57585da7bab031d3b0539fa316f1ea8330cb6b4a50ec48614fa23a4482f77cba0843c0fda9d3bd3e53476f68205f6b044b94f5097a3b6b88b93c69c2f5bf2eb46e2af25b0d9db34657dbc55e80663e77aa8a3de788f3b3d38a2925098b7a25b0760d51c57fc3365e7cef5e59a0abaee9a22c8bb0cc617413d19733c1915ef804d754b76aeb6aade395ec691748286050428376973b68ad545c2d0b35669ec5577c00e2acbec03b30335b99a9252325d62eef23d59d56beedb61b3a4d17f136e10c4ce367e60922a4a3560ee30c63b9f96fe9a787ccf3ac260772f228014ba8ab2e2e3a83eaf9d00cb0d20bc7a296aa3b3f92751772ddc33e1a8be2bba11617550f2a7a31c45e6e906f56441f02bacc55a7596f568fe3533d3e395191699f41bf360092898884677471d9cd3decd0ab035bc0d586fe7870e273419efc3bb706b2f5dfa2198591c5dc2f4b3d72856af107b3ab90d876289da7a7eb63ae4ed15eb81d857d0edd5438744978e627fb52883696976d8ab645bd3a82bd43e6be998f5a39cde116bb081755e1afc74ac84420edccabd041a4b4d1a1b4d51c190aaf30d1fac39cdb40780927a4e3536c20a4a761f1a2fcb0b270eb1e6a9f30ac44ad738595f248239503c3c28186c2ed30863656e3d125691c40a7b43fc1f8fe78d30bb3eba487ecbf425c0850249d63b3f4dbbaa340ef244441728703239fcad300ad09c8caf57b44c04f367ebf3368421111f3e68274c9784bac406406f9f1badefdd0e16a3f589d6547de38ba3f9c34e6ea5de03a7780e9a171da1f7de5216269319c5f45febab804f89a890cf76a88b295cfef9e28c095408f73abaf7aa2b892279925d3f9285a5621b020692599675a83b4960641256e799c330f33f86503894c70e902f7cb2db7fd3ac743f7f11d3cdb62b6951e3726ba1fab3b2aea5cb8185fabf51536213f17617fa9bc6421f67c57d42ff8048b6ba723cfb6df20a805de8751f35153e9c54cdd51d0e51aa51e57effc5559ebcbf80f18d54425f8b291f04e1cf1c60dcda35121e9f03d5dc781c7a2667d40c68212b526f101f9a19d97edb8463caeb4751fec201e4505fb369530cc6d78e21c43e51e1a9f9a8c7db60a9e0fc3c95fe734b33d9ac6c83a41eb9083e327231d6176ea3710d56b1f44808d1a5fb8476ea309a906dcadc08c65062f6d814ed1b45bb96cfb55e7734bbb446873fc144aa7de208eb02b5ddc6be1cf2cce4a9d368123999e66e3c8988d5f6fe1a1211684ff640fa12e25ee88df85006a976ccf354bf6a45772656592154d94714ca0a083a7372db1ca75a08b12d1def3789ad38517e3e18e4d7d4753320206d1b08ee4a39d823c3dc2effdb021db398410f0a02cab638b1987c25941faf9f08b236528784bf102bf08b54ec17dd4cb701d22b2ae4442522282f5f80c6b65c08d365dfd10fc3e9edf8027d72555a683c971d28e6e3de2ca5e5262cc19ea91265c374e36a336697c7c7de9fcc4d14db70e55b4852fb22cbe2880f73eaeffb18b80db82149e6d21225b989d7dec2097db448d48f90a655364acd7a82b00dae0471d733a7a1b529fd0f3fbe9e201e7554d92ed85e450699920ccf7e4ec46401b3291224b7ff0b01456baf7b71974b755d37a024e70f5278dfe51a2c3611f892081e1b72bf50087ac377cc9e601d2907d359202d4d56141bca9bf8f6fc41e881d0a88ccfee7b89aa31a31088ea907a6734fc2aed8df1e146b4bcdda33a98a27f82b19a85b39d262baaa4aa21193de88e13953e4fd323b330d379ad815bc8cac60e25e52e7e3c2bf7b5607e8c211905e230f5f4228e5b521c555c0056bfce80cbeec3eb127a94dde2f4e8629329e4e68d9cfd97d05bd6abf6e313eeebbadf05f30501fe9e1426d8c86a7194b8c967776808845780793e68acfa0ff2fbdd83ae93453e838a771fbe07ee2b8797c8b990f1d356c6d94a2a6dff20a65e1ec011998b8272a397fe1fe3d44a7bdd2fc00000000000000000000000000000000000000000000030d11151b21cd2e476f6c616e6720476f70686572202854657374204b657929203c6e6f2d7265706c7940676f6c616e672e636f6d3ec2ccc806136b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f02190100000000897820b11a2b609feb94175f2c42634e85cc222944b35d82adcdedd869a3e33ede7381a35db30d8517b2b6887670f9eb06a4c88eb4fb448cbb178630deadfec75d19abd95604b859ad198c619f810264baa1bcf026d103a034bb59492fa0334630e90180eaf4de2f97c2e74307c63ab08dd3dfc6e004eb1fd8d7b3cd77a9b30f93a7a810999f56648d0aa056fbfd39eecbe906347d1df15375487d0f9e5e59edcada902b16705ae3698016896806d96911924eeb6590680a60da0d37ca8a5c53a5aa6d9a679609116395155cd79caf7d13a8b4bd763893c1446f5671ac883a0af7faf9197975a3d0ab6657de3e86b639a5c5e94d4ce41206cd98fca7f1cea38cd816cbd16513fc35e39515e8c85e3fc81a63451e40255395535d908aeaa6fb4d9d892c38aec71262087fd6ead067215ed5909bb17b55dcb253e7b9889de366f647c9abdaf32f9e7de9f01904ede51c1ee0d6a4e9538e3bd8523c2e1cb2cce89ea9d897e2a8c6335c8e7eecdae7cc22d8deb23663a480a32c828b3472680a2c5e3ceaad35c66a62bd438ba6b67dbb5baebbed5b526f3277eb0efcd2b091433d388acdd7d8aeb74ae87a9bd1b0bf3e768ab54c6491d48c316d294d49a6b0248ac76bc381d4189f9cafded25b3819d7ce671dea561dc154f7d6e42587ce6f9007e4114b95b7a7ce4a356de4c7f8d4e5fed336d92ac5639fa62e36518bad391defe3b4a60e79527f88d51630e90ece4fd427d8040a706013d52dd951b086324240de23927784e26f5b9418e3e2362460b8f02d2bb7927cfd4205474371097332c1f519d7d52028a66a0b102001f03212be2797432b928d1430701c874d59134254dcadc6a45c5822a24007e50c215b7bef009f52b583df4fff749099b75df0e3369c4df8dcb3acd2cf8352ae0df3ec97302cbc739ef6da725b3742c1077fb8e0ed8a9d08ef5aa66a1644118cf5f9f4d7016892b4f9fda0bba6f40b78489d54c79f153a6fd516d7b600dbfb45046fdc3fe77c503f1180ab2fab7d71d75669f0bafabbdd39b1dc9a4695ba8d1729b79bb06e71a931c4d9e73ce37ae26a02abde595f7f8c42014c93acd7d1042f871bc7308d9f8bac3410019b3c4d8c23e7f51555b334a3f250b73c69568c76587775c9cbf3e64e6e3b75783a1c757ebbd71d1ca02d8ded33a0867bf4dc9b73eee58b469c8999b2967afca4ae5c8e0cffe867ccc584d11f45f6e7a421b36cc524ff8283d85f636e7605bf5b768582fccc5e2f55fd18a50b225c38bd60e9909f039745116d867deab8f0e55dba7fad0905d6d20a4b28a07f0827e9cc2ef2a228b7f52d98bf8babd7bfe414cbaa7010893104f181ea0b640a8dc4d2e1372cc243185be306b8e048de4672ced19e73224202c5aedecc88f9a7d8d9327ef829660f1787daf7654f3b73d3f613dada1d09eee8dca2f2134ac9fffebf0644531e5bcd1109719a119ef6d3e75903556ab4be2bb19e8b5e5af50b14f34c8c6df8b5572e164d110ef726d28cd6aa37eaf48fa8e3a31701151bfff9516d0f96e28e51cb16b3b7ae534b5d00f93364431d3d852decc6c2bf96c333b5de62daefbf57cbb380269f2497fe2d5896f9a95818de1cb753488634f47a911949aff9aafcef9ecdb6a224394e2c1ae79c647da9f347c4bb47450ceed1caaaef706127493122529278a3e4c05176dba0957dfed7ac52e27f6071e03b58babab1781e0e3487f5eb38d13d5b9b09079fa042e4c473cb449a242b9fa0b124a79822de624e04dac29c016ea2a6651037ecc102be9f5d8f140c09089d35e410ac2bf930b39050c16c83e25a5edf231edc41abb2fc0571efc2ed3c50e10339d2e470f5b3a863e308951a5a03db5bf3e960170daeb88512ee01b00b4df2cb8395a514e3746a939631375d2733cc249a00ef8e72abf28b93ca7d1a203b81483393541af0799a79725d1347ffb45464f24c0cab27c71ccbe6fef407914d1a800ca12a7b1b2336aa2bae96391ce0f82135f286817fe5234a6dd1e02d4d039ef24b1216a525ff1bd04667e1ceb6726146e7a19e38deb0e865d34130771f04ef723dd95918af07685a69ecd3e3bd7f0a80ce6e533f8ba21da3e449f780eb783150d5be04f213441fd430486c734e2c9d1549decd2921fe4323a02586b6654c5c6c976b91b9e276c7105f058f8aff7b636d2d98d8b2ac088b2bbb7d0250ebcf3dfd9142273301a12c65a3ebe33fecd0b6ba7790aad163ba1aa36f80b865b691499a13339770992d21363a431dde2269e8ebbed49df470800166a9f389dcfb5576162fef5954aa102f5e7250c0d3544b99a831d2de6c8eb2b11e23579c0b40a25bfbcafb6ec69202300f0d8c653fe8b7a03a1e08e0ac8b3528f66e0e82d3a983d6327929cd812a974e570a43bf602dd1a0b49ecc96f6ad05654c9bb78680750d2bee373003f3ef13075f6600669ea5b3b397ed92ece19ab15801607ad48ff834eae414fccec7201e2ff38d7f4583aa45865c932f3baf212622ed37cd453018a55f6820b4f1aa68fab8eb80c1121b999cc73a0ad407474b8301d3d2f92e0d8117578782c62022e3faee4c60bd47b6c9fd323c4713d70e2c731a2f31eab44454260296efd4492ed28ede6b2877106697c3b553c872c6642c521799c142da3680b6ca95dced2597f8a1cf23da27873138bcb23439e27c6c1e7a4a281ec5bb583f5aacd2da8007ed5f17d8fe9f3660629e9e285aa78911cd1a4bb01f1f667b89ca8e56fbea39153c24d88fc9021be755b1c3b66397fda7620a8d02ecf480ddcda36a6ad4aaa4154b6be9d76aacb0614960cd483138239614a7d4353df7a953bc5683a213e9786104cdb467e9711974777e3b8ef55934d826fd8cdfb392e360e3b064b845664e786568267e083c2837eeffb53a87e3211fdcb2b5c421866f8311ec63881e3e553b6fb4893deb18d9b566c89bf41e655e217076d4521ed791154209224c206213daf0bf6710660b47b3e0ae567a0ab59d991ee4ca2e7094469ec476bf2c3d919da002969c4f5e4769094b4227ff9e4500a4df2ebf5fda2924704f26835f7e8307ba3007c0988df06d34b8cb1f41e7551cdf70b514913ad44fabd3656b1dfe3bfab6ff641e449ba2289a3e97a2c16d7242c6047c3c9e5b75dccde7ce1df281bc424308cfcda584afb508df341fb41465177de002239b26033758284fad86c9ad9eaeb543d708d71b39246ac99be67315351bebbf49316186fddd6214fbcac6d334817ebbd512a631d3cd4073a9c5d6cb9095acfe0cdc755ccf660fe68ceb2e29f6807211add6824bdc72b29f2eb5ce7b4d988e0e9b62955a880108b723f183ff3805901153f5d7fb99e80f5706140e3efa83dba59c13c6aa34dbfaeb6039439f73f1fa421f349ee3340538b5ad17cb7c873754e1cbb7d149d4e4b42b0d2c69c078d3650c4a08ffa5f7f5858ad12058195770e8824a96f1086a0075598b2d3e822a76720f009fb7f5b7dd34f2e7cd2aece3eff77690cac643ca7abc312c0a075f0d9225f1c38ab51c9aafc50565e033bbc636c09f0cef28b729b50915a3c0e89bcac13f6f37abc5db66071bfd49bce6b7c774cfae9ad06bdbcd8353e947559df7cb9c1c23b311786bb20b5a9f2d818bd42feb11d275edc658310963289a80ff1319bdc24d59b27321b2b2263caa61480cc589765dfec417b763f5c5655108b4ead7636eaae6bc59aba35c2f4d9b46aaa7e62af6537b2a64cbd96454154ca782ff2818e8c1154ed12610e31c3b2191e24e319dbb18453100af44bf48e740beb3ad4f897f7f0a14fe1db045aafed727f3e18f83fe7154a9e58c1462461115b17ac07016cc86890f94a006591d319f5cec149c35646904fdb623f96d3b2da77c73bd9556f8720d17d60f3e60af44144c352415fca371ff91ab5d901a0323fffa12165238147d94ff05282622b99fe06927ddf5d442cdf9b8f0d3a2b23ba898125a808d1c452c7b200c94b14910f014debc17d1303dc08c7562c519ce31f7ad8de3e44d82e3ea1deb957773e303863f031a978427975ffa09b56fc0c145310797ec8f30ca93fff5f0dbca99910bbb9a0a9fbaabe41106c68848db67c558df693709ca06ceae6b4e7487f8ee8d8ef363b1cb4080d95a5197a73b7786cbe306774c3211197a8fcabf693c120738398f3dc1a105169d0c81d82f00fe345210849a65496440eb4f7b391ff5dd08eac2e95b439efa613b573c4bc071c8ec55e1e8b2eb5055a66c2861d82b4a8c1b096055ed2d4c2cc0992e28c1e693e43ddee5a3204830db7e1475986cb2d2cc2b452e0c57792d163c91cfe12e5aedda490cc0710e0372786bb832c8bcdf94050103de5f9ca0bdee995e0b357678637cac57f08b9d164ddd5a3657cd776acbe3c049b7e516f4b12c438e32fc902659b884a12d747a59c413679e2853eeda24984b0723acfb2af42a5e74261de28a5c2f3fab14904573b9b36ab0cbbe50172fdb8b6715a8b6977992397dede4d8fea4ff1329e520e683522ee051e9b48c68ba3ec339a05912929d50658bc3964a36397b7c6ea895eb903120b8d3b7299da38673c00bc31909f798304777ec3593ab2ddc8e8c1e1a726ec7dc46643bf96f749c6b22c625ac32e615af31107e99c52774d2d6ec0ecbd505f5167729a8f6dccf8b3b13154219eedee171f2d00466d092fc438df27cdc612e1fc1a0097e23e20debb3a7b59340abc6364e4093c57abaa29c0209206384e4e70f111d2c6a93ef3b87eb08195369c9d2fbfd1b6b80aecad5d9e7f02c4e547491d4000000000000000000000000000000070e11192228c7cd8b0651d0c68069000004c0ea397369c132ba364c46a8bbccf1684146dad886ca28f99f54dc34c960eacc7cb137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f3938500672cb54d89273a7354f988e88be3f40abaaa9dffe4138546827074fdc4c894e28d6b8b3c502ddfb5a2844a1621629def05183fa822f1591ff1b756e4ca7d8827411a32baf53301a0b321dec9a64977724fe97dced994c5950ec450c11b0c47ff826eb60abb369cc08036c8bc1937ca5b9b543ab7cdf28e3fc9a76256a89883be3536ad5757b704779c3534975b591330c43525f23790c3b819767589b468e8f272a7a598e98881f6e5395d399ce851c8f1110504ac0df1715e50286c1d462d051b4bf9d87067303259e920b3196e07bb16e86b1639fc49dc991efdd0435805782c53ce3acc64ad5a1c4281c51b98adfd80c395ccc7ea573113d1c0fc720d0b99574b712ba1d185e7d379603acd1c0001c0f61ae3326b5e481ddfe3b8bb1b4019168ecb54c90626bd80001cac44cadd74baba60abcb279ef8247099dc697c3a29a1396b8864aac1f0732d7c7cd93acea9c1bb74ca869bba69331205873c82a2ea0157b689e67c262983777114ca5a2a58cd018c8ff75c2fd54c99a577e97a3e98bb87ebf37b22b4955e0c0a557c3a93754526770dc954b38448603fb9bccd3a8caff08c11a98fe387c19d5b5e1cf66b60792e57dc240eb1408e6b6c1525848b4022a5855d16354baad6c21b2c2b55791c93f228827241186565ddc936cd9aa2f00029c296a85d085810851edbd094d5026f5485256b9963cea49dbd06b993dc85df91c74de8837985571db3a9b6c326786b15e4d23c093998f617962e8a93c7c013a006ca726b8139f471470902c7a031cfaa9a0cd3ae2ff86884105d804ac20ea73a4694c532dc73e75c5ae07c2a4e1a1c408b8003a30d2018224818a664b3106a2a52cce9701cfc869b0265c9a26129a7537b601bc902abff8707254c63e68093da20893b92c41a6cad5afcc83cb32395cb75b90c245603cfc41a8b922a0dec57258a8a806fb8b1d7965eb0148f278452aab694a0b81c07fab69cf32d1ebb13de46001f6c474b7c7db9f93065c0c5d76b2f40bcab387a2cb7701dbc1a702a29445313a58104c6c18568b3100f83160f6a96202bdb0e0b2c815bc08a083878f3ba4958f6577b7a1e27f241623c78ba7246959c458a772883086098227b5822cf43d71d9c3bbd01a83a8e53b8ddd9c312850081708e0e54b8c2d59fa00aa737ac57ca81bc826b0434f614d7863d272c9412a7389f46314365805b908e21eca6c8d0c0b76022a33b9f40da75db7285efda96fa127e3cd967a748a4d2b65a39e0947288149c42135744aab04c09ca29bb34e8baa2a225b5c68060c00f085b710d57bbf8daa4b021b17820557a4c1d7699a3a45606227028333977c3a89845e35514cb97eea978ffe47087d7226c2a544a3700eaf05eb2210572e48f97588ce5715c0da02a502183b6fab5af5285a687645fd00a7d100cbeac37707769665815f19c2169616e88b34c784c28bab24b819656a664752a3080a8311bf8bb3a4f267dca286cbea494f5bacb80a39fab2996302c1ef9eb4e42b640eff69d7a187959986128a89162ea372ae2b9b750cf7d6942c921b699764ccbf9c0c8b7614e6a31f88aa517c76dfe229fc63860615a3e63248fac6b6d55b457a561be606ccc08027fb9e404e9815474907ef74c174868caef8029b5f567a9cb40d490cc214c2f992131b137a896447f3f1145f673c2c7c64f58a84c9cb0147f41968f575dd72568bb570776f62b7dfc474c18432621b372625a5610cd3bc7761c760e1f0664c62aa487e3355ea74127567d6da61bedbc7f5ee11fbe7060fe42084cabb63d738669c44e55670ae6313d47894acf6c697850c73b3748356523b9088c0680024117622d303b48e98941b36ba48a954ac9c83647a4d3cb9e8d7ba1d76a62d44cc3840544d4b69f63f933f65b8f46413b8cf162513199b405986156700fb85e0de6bb647a04b91a13855b8e26e8843ff7b64480cfc161a7fa598a5891b3eeb378ed881330d4803c3a2a3da2abe5674db81cc5afb55095cc02015baac9b60a6f56c9e72c4538d3195418847bf45e3470a4eefc446bfabd3597c36f594bb110a785066606c0a98d4c0bf77b73110686abb52d768791528c5f7fb11baae2ba2a0207438b4f30189773247366521835d40711f3b26c479bf55801ed85074e27bd4a8b63684c26a6a584661766f89c4d31b34448011f874b9dc8719cbcca7ea769c1391b2c17b8aa8dc2637b0b06953358a11675444c92c6bb189621aede0b8658f72751475f2c0604a96b025ab48ed8109f880568d5729fb6ac700a81bd8e41c9b2755e7d5b382fbb061bc05211bbaa038309c99415d0396be1dcbe9e23c853986081a25cfafc8bbc400565e8727e5749ea3a8f60147edc155e78c6ae10e4980f44be6d20cfa85722fbd0bb76b32850c551781275bdc376f8f42dadea5e8deb1a993138f6e3ac290a0340da549caabc05666ad8f4a549723e02a74e8774be423a5b16cac1113b25a2d2abe4049d1eeacae237aeb3d8ca1247b07b39904cf9c908344eedf28a705c122b4637fcf0935a24c04047cb1490ac63dca29a90297b730c5515a6850495d19c917e974f9651ace4549be806968d27aba1c0bb8d7ac9ea8842020539d658c998694304ca6509e135e045a183664c558314ab19350dc16e89c7b6732139d4458268f3427a3769abe1ab23d60cccac43c7b77147387a523cca001b2d654bc39107affa0c69d7f661a383a6101cbe7fd82b739c6a027b98b482088b056cca422ea9e75d020982b71cbbc269033f9850b999c7bfc02921e5a5ae540212847fda0b298ef444c5dcae4667223dec4398db6bf3a736a3055a7ef1bb93c0ba3efb12de178276e4cafd1a5f8c14a72b9577ddb489143aa6558a23eb4b5acafb2194f6400efb0bbe030ab7d2903b459a538414f49132192863e6aa31e47ab1820b734951c5198b33a6184879f230e32510e7437714d9c242999f869825ded9c7c3f30dd9e153c41c701e635a618896c6294bea53432a830fb80699446246d6513da4e19ff408b159599aac247c2ca80ccfd4212c32b4bd5426723990a18c3d02479aeb881f06d40b9deb888fe84000c309af778220b76f60abbfdac69e579a2bd7881380f36af139341a4a4f9e58824ae49b21a6988d909a04a19974c7069dfb44e2ac91f37a40bc9307e250326021160f32720485bbd3255809989278f062f4d23fbc140baec844127b6e469b59ec5a7bd6377a28d699fd4bb9433ba42466a9578b9e612583541850bd954dce42a51c5347ddac2503145cd1348b4a3345435469a1353a4fd068c59edae959b33e19daf5437b5c6fddbc62b65bfc9c4eb71790f5f393852955bc870677e861e61b92157937a8d44edbed4cb48a3e02a5554dfc86f3758f48e09d67ed1c17b52dad7a793ea0f85ffbf6e06dc68fd473bf77268811b77f69c2ccc806186b0a0000002c050251d0c68022a10673dc334850357ab38e9a2092533d7c11a5b90f067fd3b8d8ea13e5544851458f021b0c00000000dc802045a0f9f6618d021e28d34e240f4a148f239320e5919c901761e9fd9cae84a53e4043f854b4713a2c6e1c1ba35d94ab74d8f9e7b7023c2f543d472e4d273787544c950823f7391baa735bb6f9239a7bf4f03a158c72878011949834a79ae7c900d2879e0530ee25191c116f3f689da3b01298dc762f4c57db337afcfeb0af62c26e1e91a9dd176327932c8730a1fc71588444084bcee209d78e48ffa45d00ad2cb60025b30728add5fbc79f1217fee79886cec3f40d8542c908454a6cce372d7e4993b57f41793d2024d845f7d0c16673f1637e4d421d6ee38c598cc419bf90c1c7d6b014d6b46b878fb560153f869fa80cb3e14b1ce54fb4212bd6760ddbb08679b1b0936b38b73d630bb5678d4e43202cfc35253900c0ac546c87d5280fd3800777fdf9339790b097395cd7d3704bdc2876623db97d9fd29b4ef60fa3a2c6213c743ba42f8c43a088059c2180f0c34de7364391389e50a03039044b1032aec7f5dec127215645a66c3385aa04919ca3ba54a86765bdcda777ebba40c75390e213ae5735e0a81db22c286fe64bd05601c7ab5edec5fb2d7641186d1bce70489b053b1c6148679c2a568e8b5bc739f4672da4b22e05be62bc6015fb9a25d4dd9c5f1e962dc7c8d076aed6ef2329a0361127bf71db3da7cd70a0025fa859a37c57418c7f532f9f31d72eb35f79dea46c2875da4e24ea33ffe3efdf28832c16ab67f63a9f7c4d8415835990e8e39b2552a1bbcb3419072b5a5f3541f6491841d811265b88fd7086bff003b38da9f9d008872abbd5f91f6df692c49e54369223ea43c2536ac310a69cd5e02b576adc3cb5218c5184836d73f4832906736f7ad1dc9b46a90ef8d7c970c56b7cb4128d37282f6be4c0599ebf0bec1e9de609687fa3f5689ccc24beaf6cf48511f5043e0679bb0b69b8f8d4f8264a07b6f54f3ef5e0bbee5203e19e59e626bc16ba05acad6cec44170cebe2ecef38c1d90bcd8b389a61de41f62dd71860c154fec43de57f00c5c7e739e11a4bd0ba1ba634955d32eb32942537dcf9ba95661b4e0ba9e2a762b71d4d3df7db298523a3b12e6804f2c05d227ddd1d3dbb538a975e7a7551395996b0f650026eb88c3ecbf3174e9c95b0d07b194048759751f04bd3e1b6f34a7ff172fc070c4ace098d9cf3a78484d907b285ceb7f8e1b39e15f917dda3166397f4922207394a9f59c6111f9e678f292b5f30f6149befd6e628d4c8dc71062844d75a951dc1af30cb6c21e9433f53c39a2764f3caeb596440c91e81177dac1443a18764152f1c4ce1bb849865d807b90c13c5d0e6afac7192b8212cd7cb5e52f0ea71de6f2adedef432e4da6b1bbace0775c2f4a9ac7a52131adb0be4a2201075ce6f5f49709caafcbec4eb6ce848de9437c2636ac693e42891873db9326a52744b5ddd5ccf21e203302f8f751ddb5ce39bdb272cfc11856b5bbf0dbe29abefd6437a502a3e991351714989bae16cadf9232bbfc73ff8b9702182115638f346d0c194a2e8697f04c4d0a4b99a87e520b5255123114560e550ece9acf01631198b829a099e6c7d59264eba4b9b8d733484cf67a5a950a0c50eaf51187b4b2601ffe874fb43a5c07811cdd9a9e000bbabc400ee7673ce6e515cc69e7b029f3a98dc22406b86a99b253c6aa7902471cfd9dee9536fe61bc85556078a9b4a2423d6bbf64a96ef93ed404bf01132d5bb4c45109367065374741e9e95b44901108958527ddee7d2282b4240bf46bbd19a8e49b4f02d271e9a2ab71a23d7d21d24c072666e98461e566b884d0e120565b680b4024aa1158c59308384963fcab0628e26bc938743d15506459871381858dafa52d008f4c169949297666cc5acc10db1af5b42ee0e741858e44cd6c850448609b616ac4bdb57cd525d17441b1ca3773eea5404446dc64626732f70c00fb37eff8910f4a66d8408bb413fd6c0b1d109db0e78b73f78484ab7db8fbcc249570ef33a2ae0ccdc6b482cca1f24f855f6d2f874ccc0162201fabdfe743a6d4c9d28688ff274ffd4cffc609f363a0c03cd28b0b96d5d6fb47c274c4d8f5bc0043d029aa358e0d700e79617c844dbaf8621afe7704dcc5b93c1cde5887091e19a2b8d72c73e54713428f0a12355f0144cec4e994e747eb4da13e214e0c8ed680de3176b58fb59535116582e1925e878fffb43296eb387b34d90f83b71f8ef77bbc8d4821fe5059f6e3bcdc8ede273a5cfb3c40373640746222e6edd4b29fe11ecf3cad4b9d43b2a7a34445ebddb3fef8ab74db7cbf5be4a9a11911144f65f1bab454bf37a03d9ac2e6ffbb71072975d76f36e2729ab25b88c4e4cfb49373709998e89e359754ced6039a43bd36c0d0470ed3f064469500b933bc3b2132691feb6cf201393218834f09bb321b287d67fd8003f8c19e9e7be18aa86aa33aa5c99dc3d221dfca2ecbd584f896ceac09a5b99919525983eea0c79156dcc6d72be1120545c71d1151c27be562daf765b3d958b42d9b4965f50340db703a166d08e7862102f8beeea1924ae659174e6f2dac4362bcabdfd68e7c6a3591273491d9153fec3029134aceb487bbced28d4d4ab41d1a5055e05b35a4ca1d8e785c8209ccd7159a17946cde1c0eac9518ffd4ceed66e5dcadb2d63d413b2a2ab2995301d3b4ae5f4b2b30e8b3468006c74b514e241b24cd885f790b51a30e44a72e96e275d3bd1a198ade4bdbc9633842fc8fbc91365c1a567ea0260c52afef802608c74c19d24da5e699153a73b8dda345f3a945f616bb1b9e6c1d7c51a9c48e55dac31a523599478bcc3a805905ac4b67b821b94c1d58dadb2c0e90e8a10ccbc936082820efff63a12b19805bb7e0ab7c49ec75ff6eb9ae405f5428b0391fffd58ecab10cf2965283419feef89764cb403b69484e151a392eb0b6eda6f7a959cdc12d3b45ba71083a29567b980bb4b1972eb827d0245b86600b725673239c34f1857cdef80682061929ad43a37f38d9ae976211a52c9cda5e62b17623bfa44146f6c0fce874d35d862727f211267263044aac70a8de811ac016c4a9d0be352a8a092b9fa7b736b1ba2c837489366bf8f6d57d13e00b65fadfd646bc380dcd5606ae54c01f819c83bc21b85f15d4df4372d96b6c23b832d27545322fab94f77859a17d5a6c854fbcd2a314f6ad60d1943870b0cc71bfeeac092a02125ce49cedf3da292ae4562b0a5c47cc158fe30975bfcdaedb8b1958b483d5c19687b39d852bf98a5eec6c538644b98173936f84765f1c6986ab4e77ba995f6f30eed1bb02a179c741e8562ce8f9ad4dddf295b35d9e7e836b009443d13c5e98062f7d7d815ff4c80ef45111c10b6ccb197345d60124fe609cd8c9200148445da4e125da18524f0cfea96538b8b44933735cb6f8bab23778ec20c6efd991ccf9003349f3f9e0a14d35dc01ff0849b32d06e8702ca1f0272e581d2b6595313b46fa9fc7360608456509048902bb6260ea55efc3624c07e1bcf2c721fa43679e953de8f155f20830f9a1b0e90fec037bcf21d381eb27f50032a47fb301c3dfb78db6dffa03f95e1a69d5fd1eeec08b529af900c5a9f09db30d83c10be7913f3c1252ca0ce9213122a40eb6b8e0d128dc0a1cdfaa6de7d512aa922a56a826a4a779d4e3b087b3bfc28ff47e06f39492f3306803a660638633aecc1885acc8691a4cde4885436a0cf01981ff4d3b03d04a25416a5001e5929784feca2e07d52d5a551c199afc1c9976636d8500530b0d859b9c678ec45d4bb1d3a02ff3a25a5a02717db45649350dd7d8432cbe00a150ac354494e99964b9bfa1ea2b09bff063423d014df921c2a0c41a636a585e56f6c5ffb130d42fdaf71829f6d140999b0e35c36903fc11a6447d6cbb18cc1efe0d6b46ff89e48a995f1052a135be7ee635fc5b6d12e676ae3336226f740953c2159940c761979d5fa272da8b80d1ac1fbc8b47022336dca41267ae23c45bb2dc2ab91a988ed7c2a629fb02084eaadf055f9f59dff2da3d5ef46fc72f69a0fedbad5a329294933e40cc08b38e33927fedf24eadb4aa6e5616cb7d881468f0507a585d5205dcdf6792172285ab3494b7220f6e23405e72be42ad6834081c8c997c2362de7e8ea7a9f993b0874fb78a78217d518bb4e46306497f800e63568ee30e74e51da8391f4f5772eaf9d9804a2057c007a7190530b61f13a198b6f10c75b455d945dc9eadc2cc8d6e23682c64100dc935d72da9006d858c53c14a4e50273c2e268d4d32642b9f4de01e9c200993ff9fdcf0c1d4a7a2e8ad4f09a8ea1e78c7fd184fb00c816def1912f0d38767be6e44e5db30596f123258a82443036901afb38dc17b537ebd652a66ae45be314c82abfabd59f62a9bfae2abe1768a7d8ef4451572e9b6912dcffb5f878adb398956c901b7139aeef2da22a85465f47b01c500a4e9e4edf388b5913738144ff4a847610ae38bcf44a43f7613cf3e0cdf7db864845f8fa53532c42c33f71fdc5189742e77ef8f99ac98bf7c581923d69ae97f41f379c4c7dff0e7358b7e3d60fc52953a6614b244fea8b22a8d2c7bc63ef1372b238fd15b1b56d4f7e42c7aa720ffe45b7ff3345177c5c83d38dc283c8649e84a1e9471daa6174272f15ce25939c0ea6376c440d5705f10e53831e1063b6184db43b182034af5f49b4745fb2caecb543013f48a46f656094dbc17fde9731bfdbf58480c72d4846a71e3772b5948eddc0d45a331f376d8b9db0dcdfe4f2fe5d8b8e041bb6b9d1d7494a656c78878eb7b8d4e6344c69778e90b10a79828fa1b5b7e2fd00000000000000000b0e141f262f" + +// PQC draft test vectors +const v6Ed25519Mlkem768X25519PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGUdDGgBsAAAAgsJV1qyvdl+EenEB4IFvP5/7Ci5XJ1rk8Yh967qV1rb0A8q5N +oCO2TM6GoqWftH02oIwWpAr+kvA+4CH7N3cpPSrCrwYfGwoAAABABQJR0MaAIqEG +UjQyQjRSVAUCGc7/KG6cjkeeyIdX+VNUOImEoC19C1kCGwMCHgkDCwkHAxUKCAIW +AAUnCQIHAgAAAADhOyBW8CPDe5FreFmlonhfVhr2EPw3WFLyd6mKRhkQm3VBfw7Q +w7eermL9Cr5O7Ah0JxmIkT18jgKQr9AwWa3nm2mcbjSoib2WVzm5EiW3f3lgflfr +ySQFpSICzPl2QcAcrgjNLlBRQyB1c2VyIChUZXN0IEtleSkgPHBxYy10ZXN0LWtl +eUBleGFtcGxlLmNvbT7CmwYTGwoAAAAsBQJR0MaAIqEGUjQyQjRSVAUCGc7/KG6c +jkeeyIdX+VNUOImEoC19C1kCGQEAAAAAg2ogTEbKVVlbWsejQHkq7xo8ipM7dv6H +z2AekkJqupKVR+/oy+2j6ri+/B2K6k1v1y5quzirhs87fB5AxZC6ZoFDvC0kZOvo +14fPF07wCx0jwJVOWuRFVsVw7pQJHbNzgkIAx82LBlHQxoBpAAAEwLRbSSpvve2p +Ih3hHweqq2VdRo+7Zf7whYHyXM/UifsniwMKSrubvsmLgCyiEwMip3ZlTSxIFDaF +EMVtVvCSJ7XFZ0WslTJnZ/CENPgxbVgn6CC2b8UEb8olS3AxlSiqJSRP0OrOJdfP +WJI1A+p7Vmw1CZQq2oVPUlE96SVUrFxfk7XCYpcTpIQb+mFB4ULCesat5tud7Tau +UJpMKssUf0I74EUjahoR46pPReKzlSqfvhpgXSASZpBg8IZBY7VbgTnLInGTTnEr +rScVlDnAwcdYvuZMQYO5EjS6LOxn1aVfU+iH+Rir2AyFzsYl6ICHciPAsKKa+Sk7 +UPFBrIRG1qgn7FF0n5epHeiFCRNb87wSqlp0h+d8L3jPmDq4zoQPKDViasoHYXLD +7KoJTIxP2eGzjMRlg3oD9ph3ZnyOTIsx/4SDtxW3q+JU8RFoI0dZEdURwaoIITWi +tldtPUmtBuJshceEDSWopuwLzBuVTnYDpTy94ZtDBKmgPnmSmPOKZ6THucmiJGUm +WmAKkyo7kWAwYRsE2ZYqLzIJFmZFzRLIThipiZhR/9h2GemQklMJqYs25cEGx6FW +zXRv8Palm7yOAicH/ldHUOtU3oFIXthOatwSrQApJ7HHvksx59ZtLFtBgHm5eRmY +YleJsJLGCPssa7pK2hIwgLlmCLSAavFqYjuocWIYKLmw5vNXXRWIjPBbTpVXbUO5 +U9F/67gggSWBJXCZlfgcluO422aN22m8aONiTgZtmjcC2elci5yRKGBbeKmFTcVs +ZbpbY6ZCKFRyzbqmMGYe0mqN6lh7R5dNiBuJZQg04mYuSzWCF3mumlJTRtlN9Miy +6LyWApJSTQdgc3awS0mjUrgU1Ia0AjMFKcxJA6iHd6iAxWMbUqxOSoTOTUlMr3lt +paNGEMGpaHwMoQs99xSI1zG9pYmfeIl6LfZSwnI4LsBvNOBiUhNUC/aYIILEm7qj +Tpw5YdI+6jSl+palLlcMDzt0LgMN8rY6UlZJBGNFSAKSNSWXdFYMByKKGSCj91TD +WPlOLvWKntSLk5eLodhgmRGqx5GZECgWS4wDARY00rl17dV53GejXrUtJaYcnam5 +pKoTSaPJTuY25Kyy+oB7aHpV0vA87JaeRCsqkjcS5IQKdtceUskXNRa2f7CTrfQR +hOGk0gSA4Jx8+Fw8uGWLGJx6m1lSyWcMX5HL7hJkFhEKebYjdALGXMV1wxNiUHCI +vxCjX/AkwHEDvAN6qhULrcZlmngSbeBysOFud2a8PIS2p7RCAatO+TpFgoR+1CgV +JIdiRpM0WrMfS9iBERhtYaLH1oUjBpcV7zpgNdkT4ClfbTpgu3oPnWBogDjMXKUe +pSfFx0l1tNGRLCCFVit8xxA4Q+phutInyXUAHJiEfHIR4jxTd/FwQ3pDoKxTesY+ +XsGtVJxe9oMrXSlt6uymn6zKQlQsw8odvHhp5/NWqkCh9/xQvmIlERsVVjyJ0FNF +/+HNT9KrECCj6+cujDbEN6UmRlFvlMcxFzYaTnWa1cshSVCCa1aYZddWrDdxOwMf +ObUw8TukY7A2RqcdpmpA68SLoWwNAgtFG1xWV43yC/P3XTsqTmgHRUGboDkVs9K8 +1+Byg4jhKWcAksr2fFDB4wkkaZcB3uUOXuQQ2etC1aCrboS5vTeMVJVS+ssLkxle +KLZ3kH9pazHbNTKQWclexAe48RImOk1PlmN9HHMgUwgJI5H8e3a7cQw8x7Yh5wce +yAdhuwRGcT99CqtaQb0aeTz9xxh642roMy46rCQp2A/g1QbZIqqVe6lb4qkJ8YdM +dG4SrE3UzD3tuAyu3L9Ql79qxxdB4Jt7wp+dPETaoZba+aMWZ68ZxDEjQJcgyrN9 +XCBNcLcU+SpjBXPK13yeCdAVGUhA1c0qB4PKVY5/e07Kc8qGgyrlJCCb05OQQKWG +mmVcJnDDIZSLM4VPd3cAgWhv5rIk/BPWQ6CGps6njH1WNaI6sTr35wcfWlMahs0w +mUPkKMG0AWwT9VBCBU7huFN7Rw2DXBdQUlQDO8WzVLXFt6sZvF+XgZ840woQ8I29 +BmW55qSY2hdtMsKqkU31Nbscxa5wRsu2KSirXF3JoZkTacU/taIRmmIwGXl0zBlM +8Hp9hJOdAZAAPAYwCj8FdmD4AyDiHHDkuJsLfL80CnKck2wYbBE/BoGRKwVul1Jr +gh4KC4DS+WfKZQYam5KLAytFMUJf8TDiYYNmVr9TOVNAoCj4XKs7BQ7KZ5MMnCWi +EEsH9im2mBrHDKXLCrFK8IY54B5ae8uDKWwOuhTtlHki5CTVHHRKaorYawvMqTZ4 +HCO+6Jrj8rm7YFxhxwPihVHIl10SK2Q2tX8ygidCKc1yPBh4lKyvyryPwL6i5sM4 +sU5glM9bZgPKfHosk4uNdqZQ5FyIaohJ8aocQpr0JVQv8rp0UjBEDBqDeIhepohd +cp5KhA1kND4vQbfjusdVtgUorAqyAw0YSoeDLAfC5syaJqo8K06CM8y7O3VqB8Rs +ZJb8Eb7mGYdH9U8m3MTjestO5LcTAyqoBJvC4TTgp6F9dJ55HJ3rzFx19wMqGhLV +Abcw/JWJagrvYqTGozbiEcLheFNmKik4eGoG9mS1Ebhwhbmg5LD6kZXFK7hJOnkb +cTdz0ynSqlPk1oJkh8Pa1gVG4IWgEJISZWEb036BmTASRc5EYVetuBujMYQKuWeI +RrumhH3GiZBw1RIyrDYYMk37OHf0MLhahBeldJsqRoLcErOSu0T9xwmeczWoIDtZ +Q8794LDkCoY6wpYFF5Scq64HgmQaS5kSQH9UtTIgbLoBmQiDUIyrx8LoBqhOdQPR +0y60NWjSXLbs0VjxrIVMZmdlxH//gknkDLlSgSqbbAkG+7T9clLS44lVYD22N03n +Mil8pHWju6yYW3eFaylzI7jLEVZ5cLw15bd1JHEvRpOBxV8Fdn+p4RKoRrUN4EQm +1olEK4TsWY+uV2RCV4PEBQpOQxGZZxhMRa/AKnD3I1LjSlNh9SLXNbVIp69bPK9N +qS8MGBGeWBzEARhXea9mBiUisSFSZrwneYALPBXH0h4xerZWV2GH9bu12gwBmJbB +k64rwZg/dqDiCM16/C0Np0Aza4oTVsOJ6BrdZh70xFZq+Dizeg85TMywkl9Ma1BT +AsMOZ45sAEwIBhUX6Colkae023ouMgj1pnFV5Rc8cTSRcGUM1ZHW8AeLAwpKu5u+ +yYuALKITAyKndmVNLEgUNoUQxW1W8JIntcVnRayVMmdn8IQ0+DFtWCfoILZvxQRv +yiVLcDGVKKolJE/Q6s4l189YkjUD6ntWbDUJlCrahU9SUT3pJVSsXF+TtcJilxOk +hBv6YUHhQsJ6xq3m253tNq5QmkwqyxR/QjvgRSNqGhHjqk9F4rOVKp++GmBdIBJm +kGDwhkFjtVuBOcsicZNOcSutJxWUOcDBx1i+5kxBg7kSNLos7GfVpV9T6If5GKvY +DIXOxiXogIdyI8Cwopr5KTtQ8UGshEbWqCfsUXSfl6kd6IUJE1vzvBKqWnSH53wv +eM+YOrjOhA8oNWJqygdhcsPsqglMjE/Z4bOMxGWDegP2mHdmfI5MizH/hIO3Fber +4lTxEWgjR1kR1RHBqgghNaK2V209Sa0G4myFx4QNJaim7AvMG5VOdgOlPL3hm0ME +qaA+eZKY84pnpMe5yaIkZSZaYAqTKjuRYDBhGwTZliovMgkWZkXNEshOGKmJmFH/ +2HYZ6ZCSUwmpizblwQbHoVbNdG/w9qWbvI4CJwf+V0dQ61TegUhe2E5q3BKtACkn +sce+SzHn1m0sW0GAebl5GZhiV4mwksYI+yxrukraEjCAuWYItIBq8WpiO6hxYhgo +ubDm81ddFYiM8FtOlVdtQ7lT0X/ruCCBJYElcJmV+ByW47jbZo3babxo42JOBm2a +NwLZ6VyLnJEoYFt4qYVNxWxlultjpkIoVHLNuqYwZh7Sao3qWHtHl02IG4llCDTi +Zi5LNYIXea6aUlNG2U30yLLovJYCklJNB2BzdrBLSaNSuBTUhrQCMwUpzEkDqId3 +qIDFYxtSrE5KhM5NSUyveW2lo0YQwalofAyhCz33FIjXMb2liZ94iXot9lLCcjgu +wG804GJSE1QL9pgggsSbuqNOnDlh0j7qNKX6lqUuVwwPO3QuAw3ytjpSVkkEY0VI +ApI1JZd0VgwHIooZIKP3VMNY+U4u9Yqe1IuTl4uh2GCZEarHkZkQKBZLjAMBFjTS +uXXt1XncZ6NetS0lphydqbmkqhNJo8lO5jbkrLL6gHtoelXS8Dzslp5EKyqSNxLk +hAp21x5SyRc1FrZ/sJOt9BGE4aTSBIDgnHz4XDy4ZYsYnHqbWVLJZwxfkcvuEmQW +EQp5tiN0AsZcxXXDE2JQcIi/EKNf8CTAcQO8A3qqFQutxmWaeBJt4HKw4W53Zrw8 +hLantEIBq075OkWChH7UKBUkh2JGkzRasx9L2IERGG1hosfWhSMGlxXvOmA12RPg +KV9tOmC7eg+dYGiAOMxcpR6lJ8XHSXW00ZEsIIVWK3zHEDhD6mG60ifJdQAcmIR8 +chHiPFN38XBDekOgrFN6xj5ewa1UnF72gytdKW3q7KafrMpCVCzDyh28eGnn81aq +QKH3/FC+YiURGxVWPInQU0X/4c1P0qsQIKPr5y6MNsQ3pSZGUW+UxzEXNhpOdZrV +yyFJUIJrVphl11asN3E7Ax85tTDxO6RjsDZGpx2makDrxIuhbA0CC0UbXFZXjfIL +8/ddOypOaAdFQZugORWz0rzX4HKDiOEpZ7+6jJ8tjNCQrKgJg1wGCpAN0VnrtFrs +2l6Q0GteA6B+fwfjuRabwerw1ro7lcwOA5EiA6XO30P+pLG07ms2MCfCmwYYGwoA +AAAsBQJR0MaAIqEGUjQyQjRSVAUCGc7/KG6cjkeeyIdX+VNUOImEoC19C1kCGwwA +AAAA5kEgPwatbx3FHPIy9J9mGUEpUE03oRRPE8N4lJ2eAIMhciCEHp3BzYVGvW3O +aPYmjcu4JTREPJM6HP7yR+ZEg+Bld9lBSVmEdMJnOX2ZHOdEoRV4bm1U4aPuhrKL +/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-----` + +const v6SlhDsaMlkem768PrivateTestVector = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +GXjAz110OD7IrJykydmxG+WiVkeP7RdkMCXM5OGNJ/4aXGuTyLCMRjk4gSp6 +V+jYMc28I5EDWfTQagLFeF8d3VTpR38eZxrvj9/jbVc46wXW2ZfCVbj9l2C8 +bybOKpmtNJEFxA+Ya2AcCKtqRyFHfeDD3fNMgtn/PbP7MeOcEVEaBrl7YQQN +V/fx7tskWXF1/xmZVhYmNOrNf854GW3OunSUV8FgMyRGq4dS5W79cGhQBQ23 +sPsZj6Tof88zDbesv8+E4dqPnFlynn4BF9CIB+GJk6v9tSsNsipgGlxSNw4k +wN7Ar0kpRhnbNMk7BGj/IIAbavzMa7rEcHxKeJnCaS6YDjzr8n5WJYC5/7j9 +gvaVNbL61eZ4rEVrE1l4am1uxARHaN+UjrndnzAztyAOHzukTnAnhpRoq2x4 +eHffMeoTIc7xWIGgxg0MENyAas0FR7npxwtU7PaEkLV4NYEK1sTQ5LfccVUV +PXPgkqVVqFMlYPTpJ3zXSe4VewWmqrx7AWjPMNCyW6Va3O3Lq/oUz/5jDfGv +y7EHITH9Oe0avRDyoqfv8Waj4ps1Yb2eGgDDDCnbqlbpXKlBp6WIoIy+2Fs9 +EcYbhCt9vuYknFyD8WN4+xpbc0lPODocRoQqmmhfYedPJ6K5+nmzYRHf2Mn7 +wAp4DFB9O+zc6QFmztc5wab/PB7oIE5BL3fe5nxmne6iIPQhJVD4m/Bo6Qhb ++Guh3LhM9N2dwZnEEpwaT1P27LUuersi53ktc1mac2djj+E13JQCoex/83wa +qowIYHfkFO87dY3wR7ZmYRCUb+72Kq6AGVx3WGDk2IgGmEC4b9DeUxU2ipQl +Skn9+oI18lxjG0gMSgwYeqo80+JDF9dyheztuUUyeP1ArQKDzFnDj/K0WcDg +ELrYi0gp1lTCc5WeBH0/9O35SvJybfqRp/hQHl79vzlbHissGRidyS5doxKu +OyTwypC26m0Sr/O020/v/h3QsITFP5dShRLduE8tV3C/SrtUuvGjqWgQAHbR +kYKlU+YLBa3Dj+LqgeUwW/u2Hj5aiEBUq8zg7TAverEHQ+csbDLyHlLbSDPd +whWaNjAONWzmSUEehjnn6ujQORdtpqLDBqpfTQBbShXZBMMpE4h8G5rbPFLM +sxrNpSx42F6qkVaVULsiEaTbIBYVnrbdK258HKpg2Qydzti1lx4N16unTxIh +T45cEAJK5Jg0OkEihHQEMhzok5WWsKbQs7hyhw6CJv7xpSO7Sh6pvB3Y1Djg +qwBbad3i7yRO9RI2cYnUzjzsstcqj5JPcbL78blZJ4Jv6e9ewMT74JG3bV02 +7Y+XXJm/QpW2mXxdvnC5iu2tQaKPju5JYPa/qyF00/r45994Foi+hcAEyK5O +Tl7ic9aPNHlD57EKuHdMRxvihqJvt+IoHlNJ6EvD71wUpDTyh9nRgSMS7MrM +1gC0MhO4cO2biYLMePGUpmLyK4p9ajKtTqqIZFfL7eJVTHiJEmumW5vJIoYw +GoPOj8mz8as11N2ny50Ys4pv7qhNfkEsGqHI9iiBjojXiiG4Cyi8hVGgnmfT +cWd3pEpUuiQMWBKokikyM/gYk90i01QY7caO95/zWcfKT7uBQime0krKeUaD +LY5p1KmSY/wj7acbs8PYJuW7kFQOKmP33kMEcmJ4VO1rc8F5jarBICCut56L ++H95aBsnt7ObJ5rdZMfgvfnD6sRVV7mLsXqQ4OSYhQsDPqjXgE/rlnGGjiHM +anDBIZPOFBUIlK+mmIvXahyKE6nflbRe4gcNFq5AtFz5E3yMvVKJ42eKck21 +EJwlIDlxOkc9/t8hUngoY3V41sU/Y535FkcBxc3oN9LRnAU8DN7WNIBlAlPB +edOw+lU8HZRlsOuHRQLQ2eboJSR+T890GoaIH/NgPWR9f22E3nm2qcFTd6RV +FMhyLj/uhUlFCdKxlF0aLeyMa40rO/U4yUXcMpnNMTRw7soungOjvy7Frhfb +m9ry4yST5pCNNzsgvCpJmc8hhCfFyNzp6KqBbBamz0hLMcgBQ5kZlOLcbPIM +jbOZ0iVrRojMjhwSD+WNxJNo70cCz96GOnhoXEcMJyxIDKm5s4oR/hqgoDmi +53MgZKWc8qWo8pVy7iqwRRamaXB9DLvovo+PWlSxm91i66JgcoaAyAlnY/U+ +W7aE +-----END PGP PRIVATE KEY BLOCK-----` diff --git a/openpgp/v2/subkeys.go b/openpgp/v2/subkeys.go index 3e9fc1890..5e631fae1 100644 --- a/openpgp/v2/subkeys.go +++ b/openpgp/v2/subkeys.go @@ -208,3 +208,15 @@ func (s *Subkey) LatestValidBindingSignature(date time.Time, config *packet.Conf } return } + +// IsPQ returns true if the algorithm is Post-Quantum safe +func (s *Subkey) IsPQ() bool { + switch s.PublicKey.PubKeyAlgo { + case packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448, + packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoMldsa87Ed448: + return true + default: + return false + } + +} diff --git a/openpgp/v2/write.go b/openpgp/v2/write.go index 398f63f47..6144153a9 100644 --- a/openpgp/v2/write.go +++ b/openpgp/v2/write.go @@ -594,8 +594,8 @@ func encrypt( encryptKeys := make([]Key, len(to)+len(toHidden)) config := params.Config - // AEAD is used if every key supports it - aeadSupported := true + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil var intendedRecipients []*packet.Recipient // Intended Recipient Fingerprint subpacket SHOULD be used when creating a signed and encrypted message @@ -610,6 +610,8 @@ func encrypt( // Override the time to select the encryption key with the provided one. timeForEncryptionKey = *params.EncryptionTime } + + allPQ := len(encryptKeys) > 0 for i, recipient := range append(to, toHidden...) { var ok bool encryptKeys[i], ok = recipient.EncryptionKey(timeForEncryptionKey, config) @@ -617,6 +619,10 @@ func encrypt( return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := recipient.PrimarySelfSignature(timeForEncryptionKey, config) if primarySelfSignature == nil { return nil, errors.StructuralError("entity without a self-signature") @@ -643,8 +649,12 @@ func encrypt( candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -1047,6 +1057,14 @@ func acceptableHashesToWrite(singingKey *packet.PublicKey) []uint8 { } } } + case packet.PubKeyAlgoMldsa65Ed25519, packet.PubKeyAlgoSlhdsaShake128s, packet.PubKeyAlgoSlhdsaShake128f: + return []uint8{ + hashToHashId(crypto.SHA3_256), + } + case packet.PubKeyAlgoMldsa87Ed448, packet.PubKeyAlgoSlhdsaShake256s: + return []uint8{ + hashToHashId(crypto.SHA3_512), + } } return []uint8{ hashToHashId(crypto.SHA256), diff --git a/openpgp/v2/write_test.go b/openpgp/v2/write_test.go index f3c4f9da7..69609b3d4 100644 --- a/openpgp/v2/write_test.go +++ b/openpgp/v2/write_test.go @@ -6,6 +6,7 @@ package v2 import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -434,33 +435,6 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { - keyRingHex string - isSigned bool - okV6 bool -}{ - { - testKeys1And2PrivateHex, - false, - true, - }, - { - testKeys1And2PrivateHex, - true, - true, - }, - { - dsaElGamalTestKeysHex, - false, - false, - }, - { - dsaElGamalTestKeysHex, - true, - false, - }, -} - func TestIntendedRecipientsEncryption(t *testing.T) { var config = &packet.Config{ V6Keys: true, @@ -674,129 +648,176 @@ func TestMultiSignEncryption(t *testing.T) { } } -func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) +var testEncryptionTests = map[string]struct { + keyRingHex string + isSigned bool + okV6 bool +}{ + "Simple": { + testKeys1And2PrivateHex, + false, + true, + }, + "Simple_signed": { + testKeys1And2PrivateHex, + true, + true, + }, + "DSA_ElGamal": { + dsaElGamalTestKeysHex, + false, + false, + }, + "DSA_ElGamal_signed": { + dsaElGamalTestKeysHex, + true, + false, + }, + // TODO: Add test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ +} - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) +func TestEncryption(t *testing.T) { + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - config := allowAllAlgorithmsConfig - config.DefaultCompressionAlgo = compAlgo - config.CompressionConfig = compConf - - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed []*Entity + if test.isSigned { + signed = kring[:1] } - config.AEADConfig = &aeadConf - } - var signers []*Entity - if signed != nil { - signers = []*Entity{signed} - } - w, err := Encrypt(buf, kring[:1], nil, signers, nil /* no hints */, &config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } - - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + config := allowAllAlgorithmsConfig + config.DefaultCompressionAlgo = compAlgo + config.CompressionConfig = compConf + config.DefaultCipher = packet.CipherAES256 + + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], nil, signed, nil /* no hints */, &config) + if (err != nil) == (test.okV6 && config.AEAD() != nil) { + // ElGamal is not allowed with v6 + return + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := signKey.PublicKey.KeyId - if len(md.SignatureCandidates) < 1 { - t.Error("no candidate signature found") + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) } - if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignatureCandidates[0].SignedByEntity == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") - encryptKey, _ := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, &config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime, &allowAllAlgorithmsConfig) + expectedKeyId := signKey.PublicKey.KeyId + if len(md.SignatureCandidates) < 1 { + t.Error("no candidate signature found") + } + if md.SignatureCandidates[0].IssuerKeyId != expectedKeyId { + t.Errorf("#%s: message signed by wrong key id, got: %v, want: %v", name, *md.SignatureCandidates[0].SignedBy, expectedKeyId) + } + if md.SignatureCandidates[0].SignedByEntity == nil { + t.Errorf("#%s: failed to find the signing Entity", name) + } + } - if test.isSigned { - if !md.IsVerified { - t.Errorf("not verified despite all data read") + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) } - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + + encryptKey, out := kring[0].EncryptionKey(testTime, &allowAllAlgorithmsConfig) + if !out { + t.Fatalf("#%s: No encryption key found", name) } - if md.Signature == nil { - t.Error("signature missing") + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - } + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) + } + + if test.isSigned { + if !md.IsVerified { + t.Errorf("not verified despite all data read") + } + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -997,3 +1018,90 @@ FindKey: } return nil } + +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + MinRSABits: 1024, + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, []*Entity{entity}, msg, c) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, _, err = VerifyDetachedSignature(entityList, msg, sig, c) + if err != nil { + t.Fatal(err) + } +} diff --git a/openpgp/write.go b/openpgp/write.go index b0f6ef7b0..542a40f20 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -279,6 +279,11 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)") } + if signer != nil { + // Handle signature algorithms that force a specific hash. + hash = signer.PubKeyAlgo.HandleSpecificHash(hash) + } + var salt []byte if signer != nil { var opsVersion = 3 @@ -391,6 +396,7 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // AEAD is used only if config enables it and every key supports it aeadSupported := config.AEAD() != nil + allPQ := len(to) > 0 for i := range to { var ok bool encryptKeys[i], ok = to[i].EncryptionKey(config.Now()) @@ -398,6 +404,10 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En return nil, errors.InvalidArgumentError("cannot encrypt a message to key id " + strconv.FormatUint(to[i].PrimaryKey.KeyId, 16) + " because it has no valid encryption keys") } + if !encryptKeys[i].PublicKey.IsPQ() { + allPQ = false + } + primarySelfSignature, _ := to[i].PrimarySelfSignature() if primarySelfSignature == nil { return nil, errors.InvalidArgumentError("entity without a self-signature") @@ -424,8 +434,12 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En candidateHashes = []uint8{hashToHashId(crypto.SHA256)} } if len(candidateCipherSuites) == 0 { - // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 - candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + if allPQ { + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}} + } else { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} + } } cipher := packet.CipherFunction(candidateCiphers[0]) @@ -560,11 +574,12 @@ func (s signatureWriter) Close() error { func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { sigLifetimeSecs := config.SigLifetime() + hash := signer.PubKeyAlgo.HandleSpecificHash(config.Hash()) return &packet.Signature{ Version: signer.Version, SigType: sigType, PubKeyAlgo: signer.PubKeyAlgo, - Hash: config.Hash(), + Hash: hash, CreationTime: config.Now(), IssuerKeyId: &signer.KeyId, IssuerFingerprint: signer.Fingerprint, diff --git a/openpgp/write_test.go b/openpgp/write_test.go index c928236b0..bcda90846 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -6,6 +6,7 @@ package openpgp import ( "bytes" + "crypto" "crypto/rand" "io" mathrand "math/rand" @@ -263,6 +264,91 @@ func TestNewEntity(t *testing.T) { } } +func TestEncryptWithAEAD(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoAEAD, + DefaultCipher: packet.CipherAES256, + AEADConfig: &packet.AEADConfig{ + DefaultMode: packet.AEADMode(1), + }, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddEncryptionSubkey(c) + if err != nil { + t.Fatal(err) + } + + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + buf := bytes.NewBuffer(nil) + w, err := Encrypt(buf, entityList[:], nil, nil, c) + if err != nil { + t.Fatal(err) + } + + const message = "test" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + m, err := ReadMessage(buf, entityList, nil /* no prompt */, c) + if err != nil { + t.Fatal(err) + } + dec, err := io.ReadAll(m.decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(dec, []byte(message)) { + t.Error("decrypted does not match original") + } +} + +func TestSignWithHMAC(t *testing.T) { + c := &packet.Config{ + Algorithm: packet.ExperimentalPubKeyAlgoHMAC, + DefaultHash: crypto.SHA512, + } + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024}) + if err != nil { + t.Fatal(err) + } + + err = entity.AddSigningSubkey(c) + if err != nil { + t.Fatal(err) + } + list := make([]*Entity, 1) + list[0] = entity + entityList := EntityList(list) + + msgBytes := []byte("message") + msg := bytes.NewBuffer(msgBytes) + sig := bytes.NewBuffer(nil) + + err = DetachSign(sig, entity, msg, nil) + if err != nil { + t.Fatal(err) + } + + msg = bytes.NewBuffer(msgBytes) + _, err = CheckDetachedSignature(entityList, msg, sig, nil) + if err != nil { + t.Fatal(err) + } +} + func TestEncryptWithCompression(t *testing.T) { kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) passphrase := []byte("passphrase") @@ -436,148 +522,162 @@ func TestSymmetricEncryptionSEIPDv2RandomizeSlow(t *testing.T) { } } -var testEncryptionTests = []struct { +var testEncryptionTests = map[string]struct { keyRingHex string isSigned bool okV6 bool }{ - { + "Simple": { testKeys1And2PrivateHex, false, true, }, - { + "Simple_signed": { testKeys1And2PrivateHex, true, true, }, - { + "DSA_ElGamal": { dsaElGamalTestKeysHex, false, false, }, - { + "DSA_ElGamal_signed": { dsaElGamalTestKeysHex, true, false, }, + //TODO: Update test vectors + /*"v6_Ed25519_ML-KEM-768+X25519": { + v6Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_Ed25519_ML-KEM-768+X25519_signed": { + v6Ed25519Mlkem768X25519PrivateHex, + true, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + false, + true, + }, + "v6_ML-DSA-67+Ed25519_ML-KEM-768+X25519_signed": { + mldsa65Ed25519Mlkem768X25519PrivateHex, + true, + true, + },*/ } func TestEncryption(t *testing.T) { - for i, test := range testEncryptionTests { - kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) - - passphrase := []byte("passphrase") - for _, entity := range kring { - if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { - err := entity.PrivateKey.Decrypt(passphrase) - if err != nil { - t.Errorf("#%d: failed to decrypt key", i) - } - } - for _, subkey := range entity.Subkeys { - if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { - err := subkey.PrivateKey.Decrypt(passphrase) + for name, test := range testEncryptionTests { + t.Run(name, func(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex)) + + passphrase := []byte("passphrase") + for _, entity := range kring { + if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { + err := entity.PrivateKey.Decrypt(passphrase) if err != nil { - t.Errorf("#%d: failed to decrypt subkey", i) + t.Fatal("Failed to decrypt key") + } + } + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { + err := subkey.PrivateKey.Decrypt(passphrase) + if err != nil { + t.Fatal("Failed to decrypt subkey") + } } } } - } - - var signed *Entity - if test.isSigned { - signed = kring[0] - } - - buf := new(bytes.Buffer) - // randomized compression test - compAlgos := []packet.CompressionAlgo{ - packet.CompressionNone, - packet.CompressionZIP, - packet.CompressionZLIB, - } - compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] - level := mathrand.Intn(11) - 1 - compConf := &packet.CompressionConfig{Level: level} - var config = &packet.Config{ - DefaultCompressionAlgo: compAlgo, - CompressionConfig: compConf, - } - // Flip coin to enable AEAD mode - if mathrand.Int()%2 == 0 { - aeadConf := packet.AEADConfig{ - DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + var signed *Entity + if test.isSigned { + signed = kring[0] } - config.AEADConfig = &aeadConf - } - w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) - if (err != nil) == (test.okV6 && config.AEAD() != nil) { - // ElGamal is not allowed with v6 - continue - } - - if err != nil { - t.Errorf("#%d: error in Encrypt: %s", i, err) - continue - } + buf := new(bytes.Buffer) + // randomized compression test + compAlgos := []packet.CompressionAlgo{ + packet.CompressionNone, + packet.CompressionZIP, + packet.CompressionZLIB, + } + compAlgo := compAlgos[mathrand.Intn(len(compAlgos))] + level := mathrand.Intn(11) - 1 + compConf := &packet.CompressionConfig{Level: level} + var config = &packet.Config{ + DefaultCompressionAlgo: compAlgo, + CompressionConfig: compConf, + DefaultCipher: packet.CipherAES256, + } - const message = "testing" - _, err = w.Write([]byte(message)) - if err != nil { - t.Errorf("#%d: error writing plaintext: %s", i, err) - continue - } - err = w.Close() - if err != nil { - t.Errorf("#%d: error closing WriteCloser: %s", i, err) - continue - } + // Flip coin to enable AEAD mode + if test.okV6 && (mathrand.Int()%2 == 0) { + aeadConf := packet.AEADConfig{ + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], + } + config.AEADConfig = &aeadConf + } - md, err := ReadMessage(buf, kring, nil /* no prompt */, config) - if err != nil { - t.Errorf("#%d: error reading message: %s", i, err) - continue - } + w, err := Encrypt(buf, kring[:1], signed, nil /* no hints */, config) + if err != nil { + t.Fatalf("Error in Encrypt: %s", err) + } - testTime, _ := time.Parse("2006-01-02", "2013-07-01") - if test.isSigned { - signKey, _ := kring[0].SigningKey(testTime) - expectedKeyId := signKey.PublicKey.KeyId - if md.SignedByKeyId != expectedKeyId { - t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId) + const message = "testing" + _, err = w.Write([]byte(message)) + if err != nil { + t.Fatalf("Error writing plaintext: %s", err) } - if md.SignedBy == nil { - t.Errorf("#%d: failed to find the signing Entity", i) + err = w.Close() + if err != nil { + t.Fatalf("Error closing WriteCloser: %s", err) } - } - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - t.Errorf("#%d: error reading encrypted contents: %s", i, err) - continue - } + md, err := ReadMessage(buf, kring, nil /* no prompt */, config) + if err != nil { + t.Fatalf("Error reading message: %s", err) + } - encryptKey, _ := kring[0].EncryptionKey(testTime) - expectedKeyId := encryptKey.PublicKey.KeyId - if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { - t.Errorf("#%d: expected message to be encrypted to %v, but got %#v", i, expectedKeyId, md.EncryptedToKeyIds) - } + testTime, _ := time.Parse("2006-01-02", "2013-07-01") + if test.isSigned { + signKey, _ := kring[0].SigningKey(testTime) + expectedKeyId := signKey.PublicKey.KeyId + if md.SignedByKeyId != expectedKeyId { + t.Errorf("Message signed by wrong key id, got: %v, want: %v", *md.SignedBy, expectedKeyId) + } + if md.SignedBy == nil { + t.Error("#Failed to find the signing Entity") + } + } - if string(plaintext) != message { - t.Errorf("#%d: got: %s, want: %s", i, string(plaintext), message) - } + plaintext, err := io.ReadAll(md.UnverifiedBody) + if err != nil { + t.Fatalf("Error reading encrypted contents: %s", err) + } - if test.isSigned { - if md.SignatureError != nil { - t.Errorf("#%d: signature error: %s", i, md.SignatureError) + encryptKey, _ := kring[0].EncryptionKey(testTime) + expectedKeyId := encryptKey.PublicKey.KeyId + if len(md.EncryptedToKeyIds) != 1 || md.EncryptedToKeyIds[0] != expectedKeyId { + t.Errorf("Expected message to be encrypted to %v, but got %#v", expectedKeyId, md.EncryptedToKeyIds) } - if md.Signature == nil { - t.Error("signature missing") + + if string(plaintext) != message { + t.Errorf("#Got: %s, want: %s", string(plaintext), message) } - } + + if test.isSigned { + if md.SignatureError != nil { + t.Errorf("Signature error: %s", md.SignatureError) + } + if md.Signature == nil { + t.Error("Signature missing") + } + } + }) } } @@ -698,7 +798,8 @@ ParsePackets: case *packet.EncryptedKey: // This packet contains the decryption key encrypted to a public key. switch p.Algo { - case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH: + case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH, + packet.PubKeyAlgoMlkem768X25519, packet.PubKeyAlgoMlkem1024X448: break default: continue diff --git a/pqc-test-vectors-jan-2025.txt b/pqc-test-vectors-jan-2025.txt new file mode 100644 index 000000000..e69de29bb