From: Sergey Matveev Date: Mon, 24 Feb 2025 11:07:39 +0000 (+0300) Subject: Another key rotation/ratcheting/commitment revise X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=f0f8e2752af924396c6aaf14dc6d09be6067ee35fa88c1167ecd1322b53192ad;p=keks.git Another key rotation/ratcheting/commitment revise --- diff --git a/go/cm/cmd/enctool/main.go b/go/cm/cmd/enctool/main.go index 3dbf2d3..72f0aa5 100644 --- a/go/cm/cmd/enctool/main.go +++ b/go/cm/cmd/enctool/main.go @@ -33,6 +33,7 @@ import ( "github.com/google/uuid" "go.cypherpunks.su/balloon/v3" "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20poly1305" "golang.org/x/term" "go.cypherpunks.su/keks" @@ -40,7 +41,7 @@ import ( cmenc "go.cypherpunks.su/keks/cm/enc" cmballoon "go.cypherpunks.su/keks/cm/enc/balloon" ballooncost "go.cypherpunks.su/keks/cm/enc/balloon/cost" - chaPoly "go.cypherpunks.su/keks/cm/enc/chapoly" + chapoly "go.cypherpunks.su/keks/cm/enc/chapoly" mceliece6960119x25519 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519" mceliece6960119 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519/mceliece6960119" sntrup4591761x25519 "go.cypherpunks.su/keks/cm/enc/sntrup4591761-x25519" @@ -100,7 +101,7 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { return } } - if encrypted.DEM.A != cmenc.ChaCha20Poly1305 { + if encrypted.DEM.A != chapoly.DEMAlgo { err = errors.New("unsupported prv encryption DEM") return } @@ -112,16 +113,12 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { } passwd := readPasswd("Passphrase for private key:") var cek []byte - cek, err = cmballoon.Decapsulate( - encrypted.KEM[0], - encrypted.Salt[:], - passwd, - ) + cek, err = cmballoon.Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd) if err != nil { return } var buf bytes.Buffer - _, err = chaPoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1) + _, err = chapoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1) if err != nil { return } @@ -144,7 +141,7 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { func main() { log.SetFlags(log.Lshortfile) flag.Usage = usage - setSalt := flag.String("salt", "", "Set that /salt instead of autogeneration") + setSalt := flag.String("id", "", "Set that /id instead of autogeneration") includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`) passphrase := flag.Bool("p", false, "Use passphrase") balloonS := flag.Int("balloon-s", 1<<17, "Balloon's space cost") @@ -152,7 +149,7 @@ func main() { balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads") doDecrypt := flag.Bool("d", false, "Decrypt") parallel := flag.Int("parallel", cmhash.DefaultNumCPU, "Parallel cryptors") - noblob := flag.Bool("no-stream", false, "Include payload into container") + noblob := flag.Bool("embed", false, "Include payload into container") flag.Parse() fdPubR := os.NewFile(FdPubR, "pub-in") @@ -224,7 +221,7 @@ func main() { log.Fatal(err) } } - if encrypted.DEM.A != cmenc.ChaCha20Poly1305 { + if encrypted.DEM.A != chapoly.DEMAlgo { log.Fatalln("unsupported DEM:", encrypted.DEM.A) } if len(encrypted.KEM) == 0 { @@ -238,16 +235,12 @@ func main() { continue } passwd := readPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:") - cek, err = cmballoon.Decapsulate( - kem, - encrypted.Salt[:], - passwd, - ) + cek, err = cmballoon.Decapsulate(kem, encrypted.Id[:], passwd) if err != nil { log.Print(err) continue } - if len(cek) != chaPoly.KeyLen { + if len(cek) != chapoly.CEKLen { log.Println(kemIdx, kem.A, "wrong key len, skipping") continue } @@ -318,24 +311,24 @@ func main() { prk, string(append( []byte(cmenc.SNTRUP4591761X25519Info), - encrypted.Salt[:]..., + encrypted.Id[:]..., )), - chaPoly.KeyLen, + chacha20poly1305.KeySize, ) if err != nil { log.Fatal(err) } - var cekp bytes.Buffer - _, err = chaPoly.Open(&cekp, bytes.NewReader(kem.CEK), kek, 1) + var cekp []byte + cekp, err = chapoly.Unwrap(kek, kem.CEK) if err != nil { log.Println(kemIdx, kem.A, err, ", skipping") continue } - if cekp.Len() != chaPoly.KeyLen { + if len(cekp) != chapoly.CEKLen { log.Println(kemIdx, kem.A, "wrong key len, skipping") continue } - cek = cekp.Bytes() + cek = cekp break } } @@ -414,24 +407,24 @@ func main() { prk, string(append( []byte(cmenc.ClassicMcEliece6960119X25519Info), - encrypted.Salt[:]..., + encrypted.Id[:]..., )), - chaPoly.KeyLen, + chacha20poly1305.KeySize, ) if err != nil { log.Fatal(err) } - var cekp bytes.Buffer - _, err = chaPoly.Open(&cekp, bytes.NewReader(kem.CEK), kek, 1) + var cekp []byte + cekp, err = chapoly.Unwrap(kek, kem.CEK) if err != nil { log.Println(kemIdx, kem.A, err, ", skipping") continue } - if cekp.Len() != chaPoly.KeyLen { + if len(cekp) != chapoly.CEKLen { log.Println(kemIdx, kem.A, "wrong key len, skipping") continue } - cek = cekp.Bytes() + cek = cekp break } } @@ -447,25 +440,25 @@ func main() { log.Fatal("no KEMs processed") } if len(encrypted.Payload) > 0 { - _, err = chaPoly.Open(os.Stdout, bytes.NewReader(encrypted.Payload), cek, *parallel) + _, err = chapoly.Open(os.Stdout, bytes.NewReader(encrypted.Payload), cek, *parallel) } else { - _, err = chaPoly.OpenBlob(os.Stdout, os.Stdin, cek, *parallel) + _, err = chapoly.OpenBlob(os.Stdout, os.Stdin, cek, *parallel) } if err != nil { log.Fatal(err) } } else { - var salt uuid.UUID + var id uuid.UUID if *setSalt == "" { - salt, err = uuid.NewRandom() + id, err = uuid.NewRandom() } else { - salt, err = uuid.Parse(*setSalt) + id, err = uuid.Parse(*setSalt) } if err != nil { log.Fatal(err) } var kems []cmenc.KEM - cek = make([]byte, chaPoly.KeyLen) + cek = make([]byte, chapoly.CEKLen) rand.Read(cek) if *passphrase { passwd := readPasswd("Passphrase:") @@ -475,11 +468,11 @@ func main() { log.Fatal("passphrases do not match") } } - bSalt := make([]byte, cmballoon.SaltLen) - rand.Read(bSalt) + salt := make([]byte, cmballoon.SaltLen) + rand.Read(salt) kem := cmenc.KEM{ A: cmballoon.BalloonBLAKE2bHKDF, - Salt: bSalt, + Salt: salt, BalloonCost: &ballooncost.Cost{ S: uint64(*balloonS), T: uint64(*balloonT), @@ -490,19 +483,19 @@ func main() { var kek []byte kek, err = hkdf.Expand( blake2bHash, - balloon.H(blake2bHash, passwd, bSalt, *balloonS, *balloonT, *balloonP), - string(append([]byte(cmballoon.HKDFInfo), salt[:]...)), - chaPoly.KeyLen, + balloon.H(blake2bHash, passwd, salt, *balloonS, *balloonT, *balloonP), + string(append([]byte(cmballoon.HKDFInfo), id[:]...)), + chacha20poly1305.KeySize, ) if err != nil { log.Fatal(err) } - var cekp bytes.Buffer - _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1) + var cekp []byte + cekp, err = chapoly.Wrap(kek, cek) if err != nil { log.Fatal(err) } - kem.CEK = cekp.Bytes() + kem.CEK = cekp } kems = append(kems, kem) } @@ -558,18 +551,18 @@ func main() { kek, err = hkdf.Expand( blake2bHash, prk, - string(append([]byte(cmenc.SNTRUP4591761X25519Info), salt[:]...)), - chaPoly.KeyLen, + string(append([]byte(cmenc.SNTRUP4591761X25519Info), id[:]...)), + chacha20poly1305.KeySize, ) if err != nil { log.Fatal(err) } - var cekp bytes.Buffer - _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1) + var cekp []byte + cekp, err = chapoly.Wrap(kek, cek) if err != nil { log.Fatal(err) } - kem.CEK = cekp.Bytes() + kem.CEK = cekp } if *includeTo { kem.To = pubIds[pubId] @@ -626,18 +619,18 @@ func main() { kek, err = hkdf.Expand( cmhash.NewSHAKE256, prk, - string(append([]byte(cmenc.ClassicMcEliece6960119X25519Info), salt[:]...)), - chaPoly.KeyLen, + string(append([]byte(cmenc.ClassicMcEliece6960119X25519Info), id[:]...)), + chacha20poly1305.KeySize, ) if err != nil { log.Fatal(err) } - var cekp bytes.Buffer - _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1) + var cekp []byte + cekp, err = chapoly.Wrap(kek, cek) if err != nil { log.Fatal(err) } - kem.CEK = cekp.Bytes() + kem.CEK = cekp } if *includeTo { kem.To = pubIds[pubId] @@ -656,13 +649,13 @@ func main() { log.Fatal(err) } enc := cmenc.Encrypted{ - Salt: salt, - KEM: kems, - DEM: cmenc.DEM{A: cmenc.ChaCha20Poly1305}, + Id: id, + KEM: kems, + DEM: cmenc.DEM{A: chapoly.DEMAlgo}, } if *noblob { var buf bytes.Buffer - if _, err = chaPoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil { + if _, err = chapoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil { log.Fatal(err) } enc.Payload = buf.Bytes() @@ -675,7 +668,7 @@ func main() { } } if !*noblob { - if _, err = chaPoly.SealBlob(os.Stdout, os.Stdin, cek, *parallel); err != nil { + if _, err = chapoly.SealBlob(os.Stdout, os.Stdin, cek, *parallel); err != nil { log.Fatal(err) } } diff --git a/go/cm/cmd/enctool/passphrase.t b/go/cm/cmd/enctool/passphrase.t index f738873..9ef833f 100755 --- a/go/cm/cmd/enctool/passphrase.t +++ b/go/cm/cmd/enctool/passphrase.t @@ -10,7 +10,7 @@ export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd balloonparams="-balloon-s 123 -balloon-t 2" test_expect_success "encrypting" "cmenctool $balloonparams -p \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" -test_expect_success "decrypting" "cmenctool $balloonparams -d -p \ +test_expect_success "decrypting" "cmenctool -d -p \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" diff --git a/go/cm/cmd/enctool/prv-encrypted.t b/go/cm/cmd/enctool/prv-encrypted.t index 1e83d9f..59f8c99 100755 --- a/go/cm/cmd/enctool/prv-encrypted.t +++ b/go/cm/cmd/enctool/prv-encrypted.t @@ -9,7 +9,7 @@ cmkeytool -algo sntrup4591761-x25519 -ku kem -sub A=KEY 5>$TMPDIR/enc.pub 9>$TMP dd if=/dev/urandom of=$TMPDIR/enc.data bs=12K count=1 2>/dev/null export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) balloonparams="-balloon-s 123 -balloon-t 2" -test_expect_success "key encrypting" "cmenctool -p -no-stream $balloonparams \ +test_expect_success "key encrypting" "cmenctool -p -embed $balloonparams \ <$TMPDIR/enc.prv >$TMPDIR/enc.prv.enc" test_expect_success "data encrypting" "cmenctool 4<$TMPDIR/enc.pub \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" diff --git a/go/cm/enc/balloon/decap.go b/go/cm/enc/balloon/decap.go index b37acff..56a3c0c 100644 --- a/go/cm/enc/balloon/decap.go +++ b/go/cm/enc/balloon/decap.go @@ -16,7 +16,6 @@ package balloon import ( - "bytes" "crypto/hkdf" "errors" "hash" @@ -25,6 +24,7 @@ import ( cmenc "go.cypherpunks.su/keks/cm/enc" chaPoly "go.cypherpunks.su/keks/cm/enc/chapoly" "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20poly1305" ) const ( @@ -41,7 +41,7 @@ func blake2bHash() hash.Hash { return h } -func Decapsulate(kem cmenc.KEM, encSalt, passphrase []byte) (cek []byte, err error) { +func Decapsulate(kem cmenc.KEM, id, passphrase []byte) (cek []byte, err error) { if len(kem.Salt) == 0 { return nil, errors.New("missing salt") } @@ -59,13 +59,11 @@ func Decapsulate(kem cmenc.KEM, encSalt, passphrase []byte) (cek []byte, err err int(kem.BalloonCost.T), int(kem.BalloonCost.P), ), - string(append([]byte(HKDFInfo), encSalt...)), - chaPoly.KeyLen, + string(append([]byte(HKDFInfo), id...)), + chacha20poly1305.KeySize, ) if err != nil { return nil, err } - var buf bytes.Buffer - _, err = chaPoly.Open(&buf, bytes.NewReader(kem.CEK), kek, 1) - return buf.Bytes(), err + return chaPoly.Unwrap(kek, kem.CEK) } diff --git a/go/cm/enc/chapoly/dem.go b/go/cm/enc/chapoly/dem.go index d9d2700..b23888e 100644 --- a/go/cm/enc/chapoly/dem.go +++ b/go/cm/enc/chapoly/dem.go @@ -13,67 +13,76 @@ // You should have received a copy of the GNU Lesser General Public // License along with this program. If not, see . -package dem +package chapoly import ( "bytes" "crypto/cipher" - "crypto/subtle" + "crypto/hkdf" "errors" + "hash" "io" "go.cypherpunks.su/keks" + "golang.org/x/crypto/blake2b" "golang.org/x/crypto/chacha20poly1305" ) const ( - ChunkLen = 128 * 1024 - PadLen = 32 - KeyLen = chacha20poly1305.KeySize + chacha20poly1305.NonceSize + ChunkLen = 128 * 1024 + CommitmentLen = 32 + CEKLen = blake2b.Size + DEMAlgo = "chapoly-krkc" ) -func incr(data []byte) { - for i := len(data) - 1; i >= 0; i-- { - data[i]++ - if data[i] != 0 { - return - } - } +type job struct { + bufReady chan struct{} + processed chan struct{} + keyAndCommitment []byte + buf []byte + tail bool } -type job struct { - bufReady chan struct{} - processed chan struct{} - buf []byte - nonce []byte +func blake2bHash() hash.Hash { + h, err := blake2b.New512(nil) + if err != nil { + panic(err) + } + return h } func do( w io.Writer, blob, seal bool, r io.Reader, - key []byte, + cek []byte, procs int, ) (total int64, err error) { - if len(key) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { + if len(cek) != CEKLen { return 0, errors.New("wrong CEK len") } ready := make(chan *job, procs) dones := make(chan *job, procs) - var iv []byte - key, iv = key[:chacha20poly1305.KeySize], key[chacha20poly1305.KeySize:] - var ctr []byte - var overhead int - { - var ciph cipher.AEAD - ciph, err = chacha20poly1305.New(key) - if err != nil { - return 0, err + keyAndCommitments := make(chan []byte, procs) + go func() { + ck := cek + var keyAndCommitment []byte + var errHKDF error + for { + keyAndCommitment, errHKDF = hkdf.Expand( + blake2bHash, ck, "dem-chapoly-krkc", + chacha20poly1305.KeySize+CommitmentLen) + if errHKDF != nil { + panic(errHKDF) + } + keyAndCommitments <- keyAndCommitment + ck, errHKDF = hkdf.Extract(blake2bHash, nil, ck) + if errHKDF != nil { + panic(errHKDF) + } } - ctr = make([]byte, ciph.NonceSize()) - overhead = ciph.Overhead() - } - blobChunkLen := PadLen + ChunkLen + overhead + }() + blobChunkLen := ChunkLen + chacha20poly1305.Overhead + CommitmentLen var blobDecoder *keks.BlobDecoder if seal { if blob { @@ -87,36 +96,53 @@ func do( if err != nil { return } - chaPolyPad := make([]byte, PadLen) var errUnauth error for range procs { go func() { var ciph cipher.AEAD - ciph, err = chacha20poly1305.New(key) - if err != nil { - panic(err) - } j := job{ buf: make([]byte, blobChunkLen), - nonce: make([]byte, ciph.NonceSize()), bufReady: make(chan struct{}), processed: make(chan struct{}), } - nonce := make([]byte, ciph.NonceSize()) + nonce := make([]byte, chacha20poly1305.NonceSize) ready <- &j var errOpen error for { <-j.bufReady + ciph, err = chacha20poly1305.New( + j.keyAndCommitment[:chacha20poly1305.KeySize], + ) + if err != nil { + panic(err) + } + if j.tail { + nonce[len(nonce)-1] = 0x01 + } if seal { - ciph.Seal(j.buf[:0], nonce, j.buf[:len(j.buf)-overhead], nil) + ciph.Seal( + j.buf[:0], + nonce, + j.buf[:len(j.buf)-chacha20poly1305.Overhead-CommitmentLen], + nil, + ) + copy( + j.buf[len(j.buf)-CommitmentLen:], + j.keyAndCommitment[chacha20poly1305.KeySize:], + ) } else { - j.buf, errOpen = ciph.Open(j.buf[:0], nonce, j.buf, nil) - if errOpen == nil { - if subtle.ConstantTimeCompare(j.buf[:PadLen], chaPolyPad) != 1 { - errUnauth = errors.New("bad pad") + if bytes.Equal( + j.buf[len(j.buf)-CommitmentLen:], + j.keyAndCommitment[chacha20poly1305.KeySize:], + ) { + j.buf, errOpen = ciph.Open( + j.buf[:0], nonce, j.buf[:len(j.buf)-CommitmentLen], nil, + ) + if errOpen == nil { + errUnauth = errOpen } } else { - errUnauth = errOpen + errUnauth = errors.New("commitment differs") } } j.processed <- struct{}{} @@ -139,7 +165,7 @@ func do( if len(j.buf) == 0 { n = 0 } else { - n, errW = io.Copy(w, bytes.NewReader(j.buf[PadLen:])) + n, errW = io.Copy(w, bytes.NewReader(j.buf)) } } total += n @@ -165,7 +191,7 @@ func do( } j = <-ready if seal { - n, errR = io.ReadFull(r, j.buf[PadLen:PadLen+ChunkLen]) + n, errR = io.ReadFull(r, j.buf[:ChunkLen]) } else { if blobDecoder == nil { n, errR = io.ReadFull(r, j.buf) @@ -188,19 +214,16 @@ func do( errR = nil eof = true if seal { - j.buf = j.buf[:PadLen+n+overhead] + j.buf = j.buf[:n+chacha20poly1305.Overhead+CommitmentLen] } } - if seal { - clear(j.buf[:PadLen]) - } else if n == 0 { + if !seal && n == 0 { break } if (seal && n < ChunkLen) || (!seal && n < blobChunkLen) { - ctr[len(ctr)-1] = 0x01 + j.tail = true } - subtle.XORBytes(j.nonce, ctr, iv) - incr(ctr[:len(ctr)-1]) + j.keyAndCommitment = <-keyAndCommitments j.bufReady <- struct{}{} dones <- j } diff --git a/go/cm/enc/chapoly/keywrap.go b/go/cm/enc/chapoly/keywrap.go new file mode 100644 index 0000000..eb623ea --- /dev/null +++ b/go/cm/enc/chapoly/keywrap.go @@ -0,0 +1,49 @@ +// GoKEKS/CM -- KEKS-encoded cryptographic messages +// Copyright (C) 2024-2025 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this program. If not, see . + +package chapoly + +import ( + "crypto/rand" + "errors" + + "golang.org/x/crypto/chacha20poly1305" +) + +func Wrap(kek, cek []byte) ([]byte, error) { + nonce := make([]byte, chacha20poly1305.NonceSizeX) + rand.Reader.Read(nonce) + ciph, err := chacha20poly1305.NewX(kek) + if err != nil { + return nil, err + } + return append(nonce, ciph.Seal(nil, nonce, cek, nil)...), nil +} + +func Unwrap(kek, encap []byte) ([]byte, error) { + if len(encap) <= chacha20poly1305.NonceSizeX+chacha20poly1305.Overhead { + return nil, errors.New("encap is too small") + } + ciph, err := chacha20poly1305.NewX(kek) + if err != nil { + return nil, err + } + return ciph.Open( + nil, + encap[:chacha20poly1305.NonceSizeX], + encap[chacha20poly1305.NonceSizeX:], + nil, + ) +} diff --git a/go/cm/enc/dem.go b/go/cm/enc/dem.go index 0f289b2..974f008 100644 --- a/go/cm/enc/dem.go +++ b/go/cm/enc/dem.go @@ -1,7 +1,5 @@ package encrypted -const ChaCha20Poly1305 = "chacha20poly1305" - type DEM struct { A string `keks:"a"` } diff --git a/go/cm/enc/enc.go b/go/cm/enc/enc.go index 4ec157d..effe612 100644 --- a/go/cm/enc/enc.go +++ b/go/cm/enc/enc.go @@ -6,5 +6,5 @@ type Encrypted struct { DEM DEM `keks:"dem"` KEM []KEM `keks:"kem"` Payload []byte `keks:"payload,omitempty"` - Salt uuid.UUID `keks:"salt"` + Id uuid.UUID `keks:"id"` } diff --git a/spec/cm/dem-chapoly-krkc.texi b/spec/cm/dem-chapoly-krkc.texi new file mode 100644 index 0000000..c3700a8 --- /dev/null +++ b/spec/cm/dem-chapoly-krkc.texi @@ -0,0 +1,26 @@ +@node dem-chapoly-krkc +@cindex dem-chapoly-krkc +@nodedescription ChaCha20-Poly1305 with key ratcheting and key commitment DEM +@subsubsection ChaCha20-Poly1305 with key ratcheting and key commitment DEM + +@code{cm/encrypted}'s @code{/dem/a} equals to "chapoly-krkc". + +CEK is 64 bytes long. +Data is split on 128 KiB chunks, each of which is encrypted the following way: + +@verbatim +CK0 = CEK +CKi = HKDF-Extract(BLAKE2b, salt="", ikm=CK{i-1}) +KEY || COMMITMENT = HKDF-Expand(BLAKE2b, prk=CKi, info="dem-chapoly-krkc") +ChaCha20-Poly1305(key=KEY, ad="", nonce=11*0x00 || tail-flag, data=chunk) || COMMITMENT +@end verbatim + +Chaining key (CK) advances with every chunk. 256-bits encryption key and +key commitment are derived from the chaining key. + +@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 (payload) +even empty. + +@code{/payload}'s chunk length equals to 128KiB+16+32 bytes. diff --git a/spec/cm/dem-kuznechik-ctr-hmac-kr.texi b/spec/cm/dem-kuznechik-ctr-hmac-kr.texi new file mode 100644 index 0000000..1df0f05 --- /dev/null +++ b/spec/cm/dem-kuznechik-ctr-hmac-kr.texi @@ -0,0 +1,23 @@ +@node dem-kuznechik-ctr-hmac-kr +@cindex dem-kuznechik-ctr-hmac-kr +@nodedescription Kuznechik-CTR-HMAC with key ratcheting DEM +@subsubsection Kuznechik-CTR-HMAC with key ratcheting DEM + +@code{cm/encrypted}'s @code{/dem/a} equals to "kuznechik-ctr-hmac-kr". + +CEK is 64 bytes long. +Data is split on 128 KiB chunks, each of which is encrypted the following way: + +@verbatim +CK0 = CEK +CKi = HKDF-Extract(Streebog-512, salt="", ikm=CK{i-1}) +Kenc || Kauth || KauthTail = HKDF-Expand( + Streebog-512, prk=CKi, info="dem-kuznechik-ctr-hmac-kr") +CT = Kuznechik-CTR(key=Kenc, ctr=0x00, data=chunk) +CT || HMAC(Streebog-256, key={Kauth|KauthTail}, data=CT) +@end verbatim + +@code{KauthTail} is used only in the last chunk to explicitly signal +that it is the last one. + +@code{/payload}'s chunk length equals to 128KiB+32 bytes. diff --git a/spec/cm/encrypted.cddl b/spec/cm/encrypted.cddl index cdb6596..4a888d7 100644 --- a/spec/cm/encrypted.cddl +++ b/spec/cm/encrypted.cddl @@ -1,29 +1,20 @@ -ai = text ; algorithm identifier - cm-encrypted = { + id: uuid, dem: dem, kem: [+ kem], - salt: uuid, ? payload: bytes, } -dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac +dem = dem-chapoly-krkc / dem-kuznechik-ctr-hmac-kr -dem-chacha20poly1305 = {a: "chacha20poly1305"} -dem-kuznechik-ctracpkm-hmac-hkdf = {a: "kuznechik-ctracpkm-hmac"} +dem-chapoly-krkc = {a: "chapoly-krkc"} +dem-kuznechik-ctr-hmac-kr = {a: "kuznechik-ctr-hmac-kr"} -kem = kem-generic / - kem-balloon-blake2b-hkdf / - kem-gost3410-hkdf-kexp15 / +kem = kem-balloon-blake2b-hkdf / + kem-gost3410-hkdf / kem-sntrup4591761-x25519-hkdf-blake2b / kem-mceliece6960119-x25519-hkdf-shake256 -kem-generic = { - a: ai, - cek: bytes, - * text => any -} - kem-balloon-blake2b-hkdf = { a: "balloon-blake2b-hkdf", cek: bytes, diff --git a/spec/cm/encrypted.texi b/spec/cm/encrypted.texi index b7f4fc5..be7771b 100644 --- a/spec/cm/encrypted.texi +++ b/spec/cm/encrypted.texi @@ -25,182 +25,33 @@ contains an encrypted CEK. If KEM uses public-key based cryptography, then recipient's @ref{cm-pub, public key}(s) should be provided, which may lack the signatures at all. Optional @code{/kem/*/to}, public key's fingerprint, -may provide a hint for quickly searching for the key on the recipient's -side. +may provide a hint to quickly search for the key on the recipient's side. -@code{/salt} is used in KEMs. UUIDv4 is recommended. +@code{/id} is used in KEMs for domain separation. UUIDv4 is recommended. +Can be null for privacy reasons. -@node cm-encrypted-chacha20poly1305 -@cindex cm-encrypted-chacha20poly1305 -@nodedescription cm/encrypted with ChaCha20-Poly1305 DEM -@subsection cm/encrypted with ChaCha20-Poly1305 DEM +@node Key wrapping +@cindex key wrapping +@nodedescription Key wrapping mechanisms +@subsection Key wrapping mechanisms - @code{/dem/a} equals to "chacha20poly1305". +@include cm/keywrap-xchapoly.texi +@include cm/keywrap-kexp15.texi - CEK is 32+12=44 bytes long and contains the key itself and - initialisation vector used in nonce. +@node DEM +@cindex DEM +@nodedescription Data encapsulation mechanisms +@subsection Data encapsulation mechanisms - Data is split on 128 KiB chunks which are encrypted the following way: +@include cm/dem-chapoly-krkc.texi +@include cm/dem-kuznechik-ctr-hmac-kr.texi -@verbatim -KEY || IV = CEK -ChaCha20-Poly1305(key=KEY, ad="", - nonce=IV XOR (BE(11-byte counter) || tail-flag), - data=32*0x00 || chunk) -@end verbatim +@node KEM +@cindex KEM +@nodedescription Key encapsulation mechanisms +@subsection Key encapsulation mechanisms - @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 (payload) even empty. - - @code{/payload}'s chunk length equals to 32+128KiB+16 bytes. - -@node cm-encrypted-kuznechik-ctracpkm-hmac -@cindex cm-encrypted-kuznechik-ctracpkm-hmac -@nodedescription cm/encrypted with Kuznechik-CTR-ACPKM+HMAC DEM -@subsection cm/encrypted with Kuznechik-CTR-ACPKM+HMAC DEM - - @code{/dem/a} equals to "kuznechik-ctracpkm-hmac". - CEK is 32+8+32=72 bytes long. - -@verbatim -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 and IV initialisation vector. Authentication of ciphertext is - performed with Streebog-512 (ГОСТ Р 34.11-2012) in HMAC mode. - - @code{/payload}'s chunk length equals to 128KiB bytes. - -@node cm-encrypted-balloon-blake2b-hkdf -@cindex cm-encrypted-balloon-blake2b-hkdf -@nodedescription cm/encrypted with Balloon-BLAKE2b+HKDF KEM -@subsection cm/encrypted with Balloon-BLAKE2b+HKDF KEM - - @code{/kem/*/a} equals to "balloon-blake2b-hkdf". - Recipient map must also contain additional fields: - - @table @code - @item /kem/*/cost/s: uint64 - Balloon's space cost (buffer size, number of hash-output sized blocks). - @item /kem/*/cost/t: uint64 - Balloon's time cost (number of rounds). - @item /kem/*/cost/p: uint64 - Balloon's parallel cost (number of threads). - @item /kem/*/salt: bin - Salt. - @end table - - @url{https://crypto.stanford.edu/balloon/, Balloon} memory-hardened - password hasher must be used with BLAKE2b-256 hash. - - @code{/kem/*/cek} is encrypted with - @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is - zero, tail-flag is set and CEK is KEK: - -@verbatim -KEK = HKDF-Expand(BLAKE2b, - prk=balloon(BLAKE2b, passphrase, /kem/salt, s, t, p), - info="keks/cm/encrypted/balloon-blake2b-hkdf" || /salt) -@end verbatim - -@node cm-encrypted-gost3410-hkdf-kexp15 -@cindex cm-encrypted-gost3410-hkdf-kexp15 -@nodedescription cm/encrypted with GOST R 34.10+HKDF+KExp15 KEM -@subsection cm/encrypted with GOST R 34.10+HKDF+KExp15 KEM - - @code{/kem/*/a} equals to "gost3410-hkdf-kexp15". - Recipient map must also contain additional fields: - - @table @code - @item /to/*/ukm: bytes - Additional 16-bytes keying material. - @item /to/*/pub: bytes - Sender's ephemeral 512-bit public key. - @end table - - ГОСТ Р 34.10-2012 VKO parameter set A/C ("gost3410-256A", "gost3410-512C") - must be used for DH operation, with UKM taken from the structure. VKO's - output is 512- or 1024-bit @code{BE(X)||BE(Y)} point. It is used in HKDF - and KExp15 (Р 1323565.1.017) key wrapping algorithm: - -@verbatim -PRK = HKDF-Extract(Streebog-512, salt="", ikm=VKO(..., ukm=UKM)) -KEKenv, IV, KEKauth = HKDF-Expand(Streebog-512, prk=PRK, - info="keks/cm/encrypted/gost3410-hkdf-kexp15" || /salt) -KExp15(KEKenc, KEKauth, IV, CEK) = CTR(Kenc, CEK || CMAC(Kauth, IV || CEK), IV=IV) -@end verbatim - -@node cm-encrypted-sntrup4591761-x25519-hkdf-blake2b -@cindex cm-encrypted-sntrup4591761-x25519-hkdf-blake2b -@nodedescription cm/encrypted with SNTRUP4591761+X25519+HKDF-BLAKE2b KEM -@subsection cm/encrypted with SNTRUP4591761+X25519+HKDF-BLAKE2b KEM - - @code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b". - Recipient public key with - @ref{cm-pub-sntrup4591761-x25519, @code{sntrup4591761-x25519}} - algorithm must be used. It should have "kem" key usage set. - - Recipient map must also contain additional field: - @code{/kem/*/encap: bytes} -- concatenation of 1047 bytes of Streamlined - NTRU Prime 4591^761's ciphertext with 32 bytes of ephemeral - X25519 public key. - - Recipient performs X25519 and SNTRUP computation to - derive/decapsulate two 32-byte shared keys. Then it combines - them to get the KEK decryption key of the CEK. - -@verbatim -PRK = HKDF-Extract(BLAKE2b, salt="", ikm= - sntrup4591761-sender-ciphertext || - x25519-sender-public-key || - sntrup4591761-recipient-public-key || - x25519-recipient-public-key || - sntrup4591761-shared-key || - x25519-shared-key) -KEK = HKDF-Expand(BLAKE2b, prk=PRK, - info="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" || /salt) -@end verbatim - - @code{/kem/*/cek} is encrypted with - @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is - zero, tail-flag is set and CEK is KEK. - -@node cm-encrypted-mceliece6960119-x25519-hkdf-shake256 -@cindex cm-encrypted-mceliece6960119-x25519-hkdf-shake256 -@nodedescription cm/encrypted with Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM -@subsection cm/encrypted with Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM - - @code{/kem/*/a} equals to "mceliece6960119-x25519-hkdf-shake256". - Recipient public key with - @ref{cm-pub-mceliece6960119-x25519, @code{mceliece6960119-x25519}} - algorithm must be used. It should have "kem" key usage set. - - Recipient map must also contain additional field: - @code{/kem/*/encap: bytes} -- concatenation of 194 bytes of - Classic McEliece 6960-119 ciphertext with 32 bytes of ephemeral - X25519 public key. - - Recipient performs X25519 and Classic McEliece computation to - derive/decapsulate two 32-byte shared keys. Then it combines - them to get the KEK decryption key of the CEK. - -@verbatim -PRK = HKDF-Extract(SHAKE256, salt="", ikm= - mceliece6960119-sender-ciphertext || - x25519-sender-public-key || - mceliece6960119-recipient-public-key || - x25519-recipient-public-key || - mceliece6960119-shared-key || - x25519-shared-key)[:32] -KEK = HKDF-Expand(SHAKE256, prk=PRK, - info="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" || /salt) -@end verbatim - - @code{/kem/*/cek} is encrypted with - @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is - zero, tail-flag is set and CEK is KEK. +@include cm/kem-balloon-blake2b-hkdf.texi +@include cm/kem-gost3410-hkdf.texi +@include cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi +@include cm/kem-mceliece6960119-x25519-hkdf-shake256.texi diff --git a/spec/cm/kem-balloon-blake2b-hkdf.texi b/spec/cm/kem-balloon-blake2b-hkdf.texi new file mode 100644 index 0000000..c87c5fa --- /dev/null +++ b/spec/cm/kem-balloon-blake2b-hkdf.texi @@ -0,0 +1,29 @@ +@node kem-balloon-blake2b-hkdf +@cindex kem-balloon-blake2b-hkdf +@nodedescription Balloon-BLAKE2b+HKDF KEM +@subsubsection Balloon-BLAKE2b+HKDF KEM + +@code{/kem/*/a} equals to "balloon-blake2b-hkdf". +Recipient map must also contain additional fields: + +@table @code +@item /kem/*/cost/s: uint64 + Balloon's space cost (buffer size, number of hash-output sized blocks). +@item /kem/*/cost/t: uint64 + Balloon's time cost (number of rounds). +@item /kem/*/cost/p: uint64 + Balloon's parallel cost (number of threads). +@item /kem/*/salt: bytes + Salt. +@end table + +@url{https://crypto.stanford.edu/balloon/, Balloon} memory-hardened +password hasher must be used with BLAKE2b hash. + +@verbatim +KEK = HKDF-Expand(BLAKE2b, + prk=balloon(BLAKE2b, passphrase, /kem/salt, s, t, p), + info="keks/cm/encrypted/balloon-blake2b-hkdf" || /id) +@end verbatim + +@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism. diff --git a/spec/cm/kem-gost3410-hkdf.texi b/spec/cm/kem-gost3410-hkdf.texi new file mode 100644 index 0000000..adba835 --- /dev/null +++ b/spec/cm/kem-gost3410-hkdf.texi @@ -0,0 +1,27 @@ +@node kem-gost3410-hkdf +@cindex kem-gost3410-hkdf +@nodedescription GOST R 34.10+HKDF KEM +@subsubsection GOST R 34.10+HKDF KEM + +@code{/kem/*/a} equals to "gost3410-hkdf". +Recipient map must also contain additional fields: + +@table @code +@item /to/*/ukm: bytes + Additional 16-bytes keying material. +@item /to/*/pub: bytes + Sender's ephemeral 512-bit public key. +@end table + +ГОСТ Р 34.10-2012 VKO parameter set A/C ("gost3410-256A", "gost3410-512C") +must be used for DH operation, with UKM taken from the structure. VKO's +output is 512- or 1024-bit @code{BE(X)||BE(Y)} point. It is used in HKDF +and KExp15 (Р 1323565.1.017) key wrapping algorithm: + +@verbatim +PRK = HKDF-Extract(Streebog-512, salt="", ikm=VKO(..., ukm=UKM)) +KEK= HKDF-Expand(Streebog-512, prk=PRK, + info="keks/cm/encrypted/gost3410-hkdf" || /id) +@end verbatim + +@code{/kem/*/cek} is wrapped with @ref{keywrap-kexp15} mechanism. diff --git a/spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi b/spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi new file mode 100644 index 0000000..9c00499 --- /dev/null +++ b/spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi @@ -0,0 +1,32 @@ +@node kem-mceliece6960119-x25519-hkdf-shake256 +@cindex kem-mceliece6960119-x25519-hkdf-shake256 +@nodedescription Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM +@subsubsection Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM + +@code{/kem/*/a} equals to "mceliece6960119-x25519-hkdf-shake256". +Recipient public key with +@ref{cm-pub-mceliece6960119-x25519, @code{mceliece6960119-x25519}} +algorithm must be used. It should have "kem" key usage set. + +Recipient map must also contain additional field: +@code{/kem/*/encap: bytes} -- concatenation of 194 bytes of +Classic McEliece 6960-119 ciphertext with 32 bytes of ephemeral +X25519 public key. + +Recipient performs X25519 and Classic McEliece computations to +derive/decapsulate two 32-byte shared keys. Then it combines +them to get the KEK decryption key of the CEK. + +@verbatim +PRK = HKDF-Extract(SHAKE256, salt="", ikm= + mceliece6960119-sender-ciphertext || + x25519-sender-public-key || + mceliece6960119-recipient-public-key || + x25519-recipient-public-key || + mceliece6960119-shared-key || + x25519-shared-key)[:32] +KEK = HKDF-Expand(SHAKE256, prk=PRK, + info="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" || /salt) +@end verbatim + +@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism. diff --git a/spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi b/spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi new file mode 100644 index 0000000..fca71c7 --- /dev/null +++ b/spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi @@ -0,0 +1,31 @@ +@node kem-sntrup4591761-x25519-hkdf-blake2b +@cindex kem-sntrup4591761-x25519-hkdf-blake2b +@nodedescription SNTRUP4591761+X25519+HKDF-BLAKE2b KEM +@subsubsection SNTRUP4591761+X25519+HKDF-BLAKE2b KEM + +@code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b". +Recipient public key with @ref{cm-pub-sntrup4591761-x25519, +@code{sntrup4591761-x25519}} algorithm must be used. It should have +"kem" key usage set. + +Recipient map must also contain additional field: @code{/kem/*/encap: +bytes} -- concatenation of 1047 bytes of Streamlined NTRU Prime +4591^761's ciphertext with 32 bytes of ephemeral X25519 public key. + +Recipient performs X25519 and SNTRUP computations to derive/decapsulate +two 32-byte shared keys. Then it combines them to get the KEK decryption +key of the CEK. + +@verbatim +PRK = HKDF-Extract(BLAKE2b, salt="", ikm= + sntrup4591761-sender-ciphertext || + x25519-sender-public-key || + sntrup4591761-recipient-public-key || + x25519-recipient-public-key || + sntrup4591761-shared-key || + x25519-shared-key) +KEK = HKDF-Expand(BLAKE2b, prk=PRK, + info="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" || /id) +@end verbatim + +@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism. diff --git a/spec/cm/keywrap-kexp15.texi b/spec/cm/keywrap-kexp15.texi new file mode 100644 index 0000000..d68149b --- /dev/null +++ b/spec/cm/keywrap-kexp15.texi @@ -0,0 +1,13 @@ +@node keywrap-kexp15 +@cindex keywrap-kexp15 +@nodedescription KExp15 key wrapping mechanism +@subsubsection KExp15 key wrapping mechanism + +KExp15 (Р 1323565.1.017) key wrapping mechanism uses GOST (ГОСТ) +cryptography algorithms. KEK is 32+8+32=72 bytes long. + +@verbatim +Kenc || IV || Kauth = KEK +KExp15(Kenc, Kauth, IV, CEK) = Kuznechik-CTR( + Kenc, CEK || Kuznechik-CMAC(Kauth, IV || CEK), IV=IV) +@end verbatim diff --git a/spec/cm/keywrap-xchapoly.texi b/spec/cm/keywrap-xchapoly.texi new file mode 100644 index 0000000..331740d --- /dev/null +++ b/spec/cm/keywrap-xchapoly.texi @@ -0,0 +1,13 @@ +@node keywrap-xchapoly +@cindex keywrap-xchapoly +@nodedescription XChaCha20-Poly1305 key wrapping mechanism +@subsubsection XChaCha20-Poly1305 key wrapping mechanism + +Key is encrypted using XChaCha20-Poly1305 algorithm. +Random 192-bit nonce is prepended to the ciphertext. +KEK has 256-bit length. + +@verbatim +NONCE = random(24 bytes) +NONCE || XChaCha20-Poly1305(key=KEK, ad="", nonce=NONCE, data=CEK) +@end verbatim