From: Sergey Matveev Date: Thu, 13 Feb 2025 09:07:06 +0000 (+0300) Subject: Revised HKDF usage X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=caffde1c5b02ff347d85f708951c47aa893bc2529c2d29d3b3d92ed020e533da;p=keks.git Revised HKDF usage --- diff --git a/go/cm/cmd/enctool/chapoly.go b/go/cm/cmd/enctool/chapoly.go index 67fecc7..5ebe5d3 100644 --- a/go/cm/cmd/enctool/chapoly.go +++ b/go/cm/cmd/enctool/chapoly.go @@ -26,9 +26,9 @@ import ( "golang.org/x/crypto/chacha20poly1305" ) -const ChaPolyChunkLen = 64 * 1024 +const ChaPolyChunkLen = 128 * 1024 -var ChaPolyPad = make([]byte, 16) +var ChaPolyPad = make([]byte, 32) func incr(data []byte) { for i := len(data) - 1; i >= 0; i-- { @@ -40,7 +40,11 @@ func incr(data []byte) { } func demChaPolySeal(cek []byte) error { - ciph, err := chacha20poly1305.New(cek) + if len(cek) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { + return errors.New("wrong CEK len") + } + key, iv := cek[:chacha20poly1305.KeySize], cek[chacha20poly1305.KeySize:] + ciph, err := chacha20poly1305.New(key) if err != nil { return err } @@ -54,6 +58,7 @@ func demChaPolySeal(cek []byte) error { _, e := keks.BlobEncode(bw, int64(len(out)), pr) blobErr <- e }() + ctr := make([]byte, ciph.NonceSize()) nonce := make([]byte, ciph.NonceSize()) var n int var eof bool @@ -65,10 +70,11 @@ func demChaPolySeal(cek []byte) error { } eof = true } - incr(nonce[:len(nonce)-1]) + incr(ctr[:len(ctr)-1]) if n != ChaPolyChunkLen { - nonce[len(nonce)-1] = 0x01 + ctr[len(ctr)-1] = 0x01 } + subtle.XORBytes(nonce, ctr, iv) _, err = pw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil)) if err != nil { return err @@ -84,7 +90,11 @@ func demChaPolySeal(cek []byte) error { } func demChaPolyOpen(cek []byte) error { - ciph, err := chacha20poly1305.New(cek) + if len(cek) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { + return errors.New("wrong CEK len") + } + key, iv := cek[:chacha20poly1305.KeySize], cek[chacha20poly1305.KeySize:] + ciph, err := chacha20poly1305.New(key) if err != nil { return err } @@ -123,6 +133,7 @@ func demChaPolyOpen(cek []byte) error { var eof bool var chunk []byte bw := bufio.NewWriterSize(os.Stdout, ChaPolyChunkLen) + ctr := make([]byte, ciph.NonceSize()) nonce := make([]byte, ciph.NonceSize()) for !eof { n, err = io.ReadFull(pr, in) @@ -132,10 +143,11 @@ func demChaPolyOpen(cek []byte) error { } eof = true } - incr(nonce[:len(nonce)-1]) + incr(ctr[:len(ctr)-1]) if n != len(in) { - nonce[len(nonce)-1] = 0x01 + ctr[len(ctr)-1] = 0x01 } + subtle.XORBytes(nonce, ctr, iv) chunk, err = ciph.Open(out[:0], nonce, in[:n], nil) if err != nil { return err @@ -159,8 +171,7 @@ func kemChaPolySeal(kek, cek []byte) ([]byte, error) { return nil, err } nonce := make([]byte, ciph.NonceSize()) - plaintext := append(ChaPolyPad, cek...) - return ciph.Seal(nil, nonce, plaintext, nil), nil + return ciph.Seal(nil, nonce, cek, nil), nil } func kemChaPolyOpen(kek, ciphertext []byte, cekLenExpected int) ([]byte, error) { @@ -174,10 +185,6 @@ func kemChaPolyOpen(kek, ciphertext []byte, cekLenExpected int) ([]byte, error) if err != nil { return nil, err } - if subtle.ConstantTimeCompare(cek[:len(ChaPolyPad)], ChaPolyPad) != 1 { - return nil, errors.New("bad pad") - } - cek = cek[len(ChaPolyPad):] if len(cek) != cekLenExpected { return nil, errors.New("invalid CEK len") } diff --git a/go/cm/cmd/enctool/main.go b/go/cm/cmd/enctool/main.go index 761af30..6378a10 100644 --- a/go/cm/cmd/enctool/main.go +++ b/go/cm/cmd/enctool/main.go @@ -47,8 +47,8 @@ import ( const BindFdNum = 3 + 1 -func blake2b256() hash.Hash { - h, err := blake2b.New256(nil) +func blake2bHash() hash.Hash { + h, err := blake2b.New512(nil) if err != nil { panic(err) } @@ -179,16 +179,25 @@ func main() { passwd := readPasswd("Passphrase:") { var kek []byte - kek, err = hkdf.Extract(blake2b256, balloon.H(blake2b256, - passwd, - append(encrypted.Bind[:], *kem.Salt...), - int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P), - ), []byte(cmenc.BalloonHKDFSalt)) + kek, err = hkdf.Expand( + blake2bHash, + balloon.H( + blake2bHash, + passwd, + append(encrypted.Bind[:], *kem.Salt...), + int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P), + ), + cmenc.BalloonHKDFInfo, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } var cekp []byte - cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize) + cekp, err = kemChaPolyOpen( + kek, kem.CEK, + chacha20poly1305.KeySize+chacha20poly1305.NonceSize, + ) if err != nil { log.Println(kemIdx, kem.A, err, ", skipping") continue @@ -248,18 +257,29 @@ func main() { ourX25519.PublicKey().Bytes()..., ) ikm := bytes.Join([][]byte{ - encrypted.Bind[:], *kem.Encap, pub, keySNTRUP[:], keyX25519, }, []byte{}) + var prk []byte + prk, err = hkdf.Extract(blake2bHash, ikm, encrypted.Bind[:]) + if err != nil { + log.Fatal(err) + } var kek []byte - kek, err = hkdf.Extract(blake2b256, - ikm, []byte(cmenc.SNTRUP4591761X25519Salt)) + kek, err = hkdf.Expand( + blake2bHash, + prk, + cmenc.SNTRUP4591761X25519Info, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } var cekp []byte - cekp, err = kemChaPolyOpen(kek, kem.CEK, chacha20poly1305.KeySize) + cekp, err = kemChaPolyOpen( + kek, kem.CEK, + chacha20poly1305.KeySize+chacha20poly1305.NonceSize, + ) if err != nil { log.Println(kemIdx, kem.A, err, ", skipping") continue @@ -330,13 +350,22 @@ func main() { ourX25519.PublicKey().Bytes()..., ) ikm := bytes.Join([][]byte{ - encrypted.Bind[:], *kem.Encap, pub, keyMcEliece, keyX25519, }, []byte{}) + var prk []byte + prk, err = hkdf.Extract( + cmhash.NewSHAKE256, ikm, encrypted.Bind[:]) + if err != nil { + log.Fatal(err) + } var kek []byte - kek, err = hkdf.Extract(cmhash.NewSHAKE256, - ikm, []byte(cmenc.ClassicMcEliece6960119X25519Salt)) + kek, err = hkdf.Expand( + cmhash.NewSHAKE256, + prk, + cmenc.ClassicMcEliece6960119X25519Info, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } @@ -344,7 +373,7 @@ func main() { cekp, err = kemChaPolyOpen( kek[:chacha20poly1305.KeySize], kem.CEK, - chacha20poly1305.KeySize, + chacha20poly1305.KeySize+chacha20poly1305.NonceSize, ) if err != nil { log.Println(kemIdx, kem.A, err, ", skipping") @@ -383,7 +412,7 @@ func main() { bindFd.Close() } var kems []cmenc.KEM - cek = make([]byte, chacha20poly1305.KeySize) + cek = make([]byte, chacha20poly1305.KeySize+chacha20poly1305.NonceSize) _, err = io.ReadFull(rand.Reader, cek) if err != nil { log.Fatal(err) @@ -411,11 +440,16 @@ func main() { } { var kek []byte - kek, err = hkdf.Extract(blake2b256, balloon.H(blake2b256, - passwd, - append(binding[:], salt...), - *balloonS, *balloonT, *balloonP, - ), []byte(cmenc.BalloonHKDFSalt)) + kek, err = hkdf.Expand( + blake2bHash, + balloon.H(blake2bHash, + passwd, + append(binding[:], salt...), + *balloonS, *balloonT, *balloonP, + ), + cmenc.BalloonHKDFInfo, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } @@ -466,13 +500,21 @@ func main() { kem.Encap = &encap { ikm := bytes.Join([][]byte{ - binding[:], encap, pub.V, keySNTRUP[:], keyX25519, }, []byte{}) + var prk []byte + prk, err = hkdf.Extract(blake2bHash, ikm, binding[:]) + if err != nil { + log.Fatal(err) + } var kek []byte - kek, err = hkdf.Extract(blake2b256, - ikm, []byte(cmenc.SNTRUP4591761X25519Salt)) + kek, err = hkdf.Expand( + blake2bHash, + prk, + cmenc.SNTRUP4591761X25519Info, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } @@ -525,13 +567,21 @@ func main() { kem.Encap = &encap { ikm := bytes.Join([][]byte{ - binding[:], encap, pub.V, keyMcEliece[:], keyX25519, }, []byte{}) + var prk []byte + prk, err = hkdf.Extract(cmhash.NewSHAKE256, ikm, binding[:]) + if err != nil { + log.Fatal(err) + } var kek []byte - kek, err = hkdf.Extract(cmhash.NewSHAKE256, - ikm, []byte(cmenc.ClassicMcEliece6960119X25519Salt)) + kek, err = hkdf.Expand( + cmhash.NewSHAKE256, + prk, + cmenc.ClassicMcEliece6960119X25519Info, + chacha20poly1305.KeySize, + ) if err != nil { log.Fatal(err) } diff --git a/go/cm/encrypted/balloon.go b/go/cm/encrypted/balloon.go index 1d8c9fc..87cc069 100644 --- a/go/cm/encrypted/balloon.go +++ b/go/cm/encrypted/balloon.go @@ -2,7 +2,7 @@ package encrypted const ( BalloonSaltLen = 8 - BalloonHKDFSalt = "keks/cm/encrypted/balloon-blake2b-hkdf" + BalloonHKDFInfo = "keks/cm/encrypted/balloon-blake2b-hkdf" ) type BalloonCost struct { diff --git a/go/cm/encrypted/kem.go b/go/cm/encrypted/kem.go index ff092f3..ff70920 100644 --- a/go/cm/encrypted/kem.go +++ b/go/cm/encrypted/kem.go @@ -6,8 +6,8 @@ import ( ) const ( - SNTRUP4591761X25519Salt = "keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" - ClassicMcEliece6960119X25519Salt = "keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" + SNTRUP4591761X25519Info = "keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" + ClassicMcEliece6960119X25519Info = "keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" ) type KEM struct { diff --git a/spec/format/encrypted.cddl b/spec/format/encrypted.cddl index b6ddfd6..61ae8a3 100644 --- a/spec/format/encrypted.cddl +++ b/spec/format/encrypted.cddl @@ -7,15 +7,10 @@ cm-encrypted = { ? ciphertext: blob, } -dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac-hkdf +dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac dem-chacha20poly1305 = {a: "chacha20poly1305"} - -dem-kuznechik-ctracpkm-hmac-hkdf = { - a: "kuznechik-ctracpkm-hmac-hkdf", - seed: bytes, - iv: bytes, -} +dem-kuznechik-ctracpkm-hmac-hkdf = {a: "kuznechik-ctracpkm-hmac"} kem = kem-generic / kem-balloon-blake2b-hkdf / @@ -45,7 +40,6 @@ kem-gost3410-hkdf-kexp15 = { cek: bytes, ukm: bytes, pub: bytes, - iv: bytes, ? to: uuid, } diff --git a/spec/format/encrypted.texi b/spec/format/encrypted.texi index 6c176b8..ef227ca 100644 --- a/spec/format/encrypted.texi +++ b/spec/format/encrypted.texi @@ -34,46 +34,49 @@ Either UUIDv4 or UUIDv7 are recommended. @cindex cm-encrypted-chacha20poly1305 @subsection Encrypted data with ChaCha20-Poly1305 DEM - @code{/dem/a} equals to "chacha20poly1305". Data is split on 64 KiB - chunks which are encrypted the following way: + @code{/dem/a} equals to "chacha20poly1305". + + CEK is 32+12=44 bytes long and contains the key itself and + initialisation vector used in nonce. + + Data is split on 128 KiB chunks which are encrypted the following way: @verbatim -ChaCha20-Poly1305( - key=cek, nonce=BE(11-byte counter) || tail-flag, - data=16*0x00 || chunk, ad="") +KEY || IV = CEK +ChaCha20-Poly1305(key=KEY, ad="", + nonce=IV XOR (BE(11-byte counter) || tail-flag), + data=32*0x00 || chunk) @end verbatim - where @code{counter} starts at zero and incremented with each chunk. + @code{counter} starts at zero and incremented with each chunk. + @code{tail-flag} is a byte indicating if that is the last chunk in the payload. It equals to 0x01 for the last chunk and to 0x00 for other ones. Last chunk should be smaller than previous ones, maybe even empty. - @code{/ciphertext}'s chunk length equals to 16+64KiB+16 bytes. + @code{/ciphertext}'s chunk length equals to 32+128KiB+16 bytes. -@node cm-encrypted-kuznechik-ctracpkm-hmac-hkdf -@cindex cm-encrypted-kuznechik-ctracpkm-hmac-hkdf -@subsection Encrypted data with Kuznechik-CTR-ACPKM+HMAC-HKDF DEM +@node cm-encrypted-kuznechik-ctracpkm-hmac +@cindex cm-encrypted-kuznechik-ctracpkm-hmac +@subsection Encrypted data with Kuznechik-CTR-ACPKM+HMAC DEM - @code{/dem/a} equals to "kuznechik-ctracpkm-hmac-hkdf". - @code{/dem/seed} contains 16 bytes for the HKDF invocation below. - @code{/dem/iv} contains 8 bytes of initialisation vector. + @code{/dem/a} equals to "kuznechik-ctracpkm-hmac". + CEK is 32+8+32=72 bytes long. @verbatim -Kenc, Kauth = HKDF-Extract(Streebog-512, - salt="keks/cm/encrypted/kuznechik-ctracpkm-hmac-hkdf", - secret=seed || CEK) +Kenc || IV || Kauth = CEK @end verbatim Encryption is performed with Kuznechik (ГОСТ Р 34.12-2015) block cipher in CTR-ACPKM mode of operation (Р 1323565.1.017) with 256KiB section - size. Authentication of ciphertext is performed with Streebog-512 (ГОСТ - Р 34.11-2012) in HMAC mode. + size and IV initialisation vector. Authentication of ciphertext is + performed with Streebog-512 (ГОСТ Р 34.11-2012) in HMAC mode. - @code{/ciphertext}'s chunk length equals to 64KiB bytes. + @code{/ciphertext}'s chunk length equals to 128KiB bytes. @node cm-encrypted-balloon-blake2b-hkdf @cindex cm-encrypted-balloon-blake2b-hkdf -@subsection Encrypted data with Balloon-BLAKE2b+HKDF-BLAKE2b KEM +@subsection Encrypted data with Balloon-BLAKE2b+HKDF KEM @code{/kem/*/a} equals to "balloon-blake2b-hkdf". Recipient map must also contain additional fields: @@ -95,10 +98,10 @@ Kenc, Kauth = HKDF-Extract(Streebog-512, @code{/kem/*/cek} is encrypted the following way: @verbatim -KEK = HKDF-Extract(BLAKE2b-256, - salt="keks/cm/encrypted/balloon-blake2b-hkdf", - secret=balloon(BLAKE2b-256, passphrase, bind || salt, s, t, p)) -ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") +KEK = HKDF-Expand(BLAKE2b, + prk=balloon(BLAKE2b, passphrase, bind || salt, s, t, p), + info="keks/cm/encrypted/balloon-blake2b-hkdf") +ChaCha20-Poly1305(data=CEK, key=KEK, nonce=12*0x00, ad="") @end verbatim @node cm-encrypted-gost3410-hkdf-kexp15 @@ -113,8 +116,6 @@ ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") Additional 16-bytes keying material. @item /to/*/pub: bytes Sender's ephemeral 512-bit public key. - @item /to/*/iv: bytes - 8-byte initialisation vector for KExp15. @end table ГОСТ Р 34.10-2012 VKO parameter set A/C ("gost3410-256A", "gost3410-512C") @@ -123,11 +124,10 @@ ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") and KExp15 (Р 1323565.1.017) key wrapping algorithm: @verbatim -KEKenv, KEKauth = HKDF-Extract(Streebog-512, - salt="keks/cm/encrypted/gost3410-hkdf-kexp15", - secret=bind || VKO(...)) -KExp15(KEKenc, KEKauth, IV, CEK): - return CTR(Kenc, CEK+CMAC(Kauth, IV+CEK), IV=IV) +PRK = HKDF-Extract(Streebog-512, salt=bind, ikm=VKO(..., ukm=UKM)) +KEKenv, IV, KEKauth = HKDF-Expand(Streebog-512, prk=PRK, + info="keks/cm/encrypted/gost3410-hkdf-kexp15") +KExp15(KEKenc, KEKauth, IV, CEK) = CTR(Kenc, CEK || CMAC(Kauth, IV || CEK), IV=IV) @end verbatim @node cm-encrypted-sntrup4591761-x25519-hkdf-blake2b @@ -150,16 +150,17 @@ KExp15(KEKenc, KEKauth, IV, CEK): @code{/kem/*/cek} is encrypted the following way: @verbatim -KEK = HKDF-Extract(BLAKE2b-256, - salt="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b", - secret=bind || +PRK = HKDF-Extract(BLAKE2b, salt=bind, + secret= sntrup4591761-sender-ciphertext || x25519-sender-public-key || sntrup4591761-recipient-public-key || x25519-recipient-public-key || sntrup4591761-shared-key || x25519-shared-key) -ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") +KEK = HKDF-Expand(BLAKE2b, prk=PRK, + info="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b") +ChaCha20-Poly1305(data=CEK, key=KEK, nonce=12*0x00, ad="") @end verbatim @node cm-encrypted-mceliece6960119-x25519-hkdf-shake256 @@ -182,14 +183,15 @@ ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") @code{/kem/*/cek} is encrypted the following way: @verbatim -KEK = HKDF-Extract(SHAKE256, - salt="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256", - secret=bind || +PRK = HKDF-Extract(SHAKE256, salt=bind, + secret= mceliece6960119-sender-ciphertext || x25519-sender-public-key || mceliece6960119-recipient-public-key || x25519-recipient-public-key || mceliece6960119-shared-key || x25519-shared-key)[:32] -ChaCha20-Poly1305(data=16*0x00 || CEK, key=KEK, nonce=12*0x00, ad="") +KEK = HKDF-Expand(SHAKE256, prk=PRK, + info="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256") +ChaCha20-Poly1305(data=CEK, key=KEK, nonce=12*0x00, ad="") @end verbatim diff --git a/spec/format/registry.texi b/spec/format/registry.texi index eb653f4..a925d8b 100644 --- a/spec/format/registry.texi +++ b/spec/format/registry.texi @@ -51,8 +51,8 @@ There is example registry of known algorithm identifiers. @table @code @item chacha20poly1305 @code{@ref{cm-encrypted-chacha20poly1305}} -@item kuznechik-ctracpkm-hmac-hkdf - @code{@ref{cm-encrypted-kuznechik-ctracpkm-hmac-hkdf}} +@item kuznechik-ctracpkm-hmac + @code{@ref{cm-encrypted-kuznechik-ctracpkm-hmac}} @end table @node AI KEM