From: Sergey Matveev Date: Sun, 5 Oct 2025 12:16:15 +0000 (+0300) Subject: Sender authentication implementation X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=b6ee8e0be2be1557a3e1cd2012bd0df7c55e2a69f4272286a6d31bd83cf84558;p=keks.git Sender authentication implementation --- diff --git a/go/cm/cmd/cmenctool/main.go b/go/cm/cmd/cmenctool/main.go index e0e5576..7efd191 100644 --- a/go/cm/cmd/cmenctool/main.go +++ b/go/cm/cmd/cmenctool/main.go @@ -22,13 +22,18 @@ import ( "crypto/hkdf" "crypto/rand" "crypto/sha3" + "encoding/hex" "errors" "flag" + "fmt" "hash" "io" + "io/fs" "log" "os" + "path" "strconv" + "strings" "github.com/google/uuid" "go.cypherpunks.su/balloon/v3" @@ -59,6 +64,11 @@ const ( X25519KeyLen = 32 ) +var ( + PrvDir = flag.String("prvs", "prvs", "Path to directory with private keys") + PubDir = flag.String("pubs", "pubs", "Path to directory with public keys") +) + func blake2bHash() hash.Hash { h, err := blake2b.New512(nil) if err != nil { @@ -67,7 +77,51 @@ func blake2bHash() hash.Hash { return h } -func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { +func pubParse(pth string) (pubData *sign.PubData, err error) { + var data []byte + data, err = os.ReadFile(pth) + if err != nil { + return + } + var signed *sign.Signed + signed, _, err = sign.PubParse(data) + if err != nil { + return + } + pubData = signed.PubData() + if !pubData.Can(sign.KUKEM) { + err = fmt.Errorf("does not have %s key usage", sign.KUKEM) + return + } + if len(pubData.Pub) != 1 { + err = errors.New("single public key expected") + } + return +} + +func findPub(id []byte) (pub *sign.PubData, err error) { + pth := path.Join( + *PubDir, strings.ToUpper(hex.EncodeToString(id)), + ) + if _, err = os.Stat(pth); err != nil { + if errors.Is(err, fs.ErrNotExist) { + err = nil + } + return + } + return pubParse(pth) +} + +func findPrv(id []byte) (av *cm.AV, err error) { + data, err := os.ReadFile(path.Join( + *PrvDir, strings.ToUpper(hex.EncodeToString(id)), + )) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + err = nil + } + return + } data, err = cmballoon.PossibleInteractiveDecrypt(data) if err != nil { return @@ -91,10 +145,10 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) { return } } - if err = d.UnmarshalStruct(&av); err != nil { - return + var v cm.AV + if err = d.UnmarshalStruct(&v); err == nil { + av = &v } - tail = d.B return } @@ -102,7 +156,12 @@ func main() { log.SetFlags(log.Lshortfile) flag.Usage = usage setId := flag.String("id", "", "Set that /id instead of autogeneration") - includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`) + assumeToHex := flag.String("assume-to", "", + `Assume that hexadecimal "to" value if missing`) + assumeFromHex := flag.String("assume-from", "", + `Assume that hexadecimal "from" value if missing`) + noFrom := flag.Bool("no-from", false, `Do not include "from" field in KEMs`) + noTo := flag.Bool("no-to", false, `Do not include "to" field in KEMs`) passphrase := flag.Bool("p", false, "Use passphrase") balloonS := flag.Int("balloon-s", 1<<17, "Balloon's space cost") balloonT := flag.Int("balloon-t", 4, "Balloon's time cost") @@ -110,49 +169,67 @@ func main() { doDecrypt := flag.Bool("d", false, "Decrypt") parallel := flag.Int("parallel", cmhash.DefaultNumCPU, "Parallel cryptors") noblob := flag.Bool("embed", false, "Include payload into container") - flag.Parse() - - fdPubR := os.NewFile(FdPubR, "pub-in") + fromPth := flag.String("from", "", "Path to sender's public key for authentication") var pubs []cm.AV var pubIds [][]byte - if data, err := io.ReadAll(fdPubR); err == nil { - for len(data) > 0 { - var signed *sign.Signed - signed, data, err = sign.PubParse(data) - if err != nil { - log.Fatalln("public key:", len(pubs), ":", err) - } - pubData := signed.PubData() - if !pubData.Can(sign.KUKEM) { - log.Println( - "public key:", len(pubs), ": does not have", - sign.KUKEM, "key usage", - ) - } - if len(pubData.Pub) != 1 { - log.Fatalln("public key:", len(pubs), ": expected single public key") - } - pubs = append(pubs, pubData.Pub[0]) - pubIds = append(pubIds, pubData.Id) + flag.Func("to", "Path to recipient's public key", func(v string) error { + pubData, err := pubParse(v) + if err != nil { + log.Fatalln(v, ":", err) + } + pubs = append(pubs, pubData.Pub[0]) + pubIds = append(pubIds, pubData.Id) + return nil + }) + flag.Parse() + + var err error + var assumeTo []byte + if *assumeToHex != "" { + assumeTo, err = hex.DecodeString(*assumeToHex) + if err != nil { + log.Fatal(err) + } + } + var assumeFrom []byte + if *assumeFromHex != "" { + assumeFrom, err = hex.DecodeString(*assumeFromHex) + if err != nil { + log.Fatal(err) } } - fdPubR.Close() - fdPrvR := os.NewFile(FdPrvR, "prv-in") - var prvs []*cm.AV - if data, err := io.ReadAll(fdPrvR); err == nil { - for len(data) > 0 { - var av cm.AV - av, data, err = parsePrv(data) - if err != nil { - log.Fatalln("private key:", len(prvs), ":", err) - } - prvs = append(prvs, &av) + var fromA string + var fromPrv, fromPub, fromId []byte + if *fromPth != "" { + var pubData *sign.PubData + pubData, err = pubParse(*fromPth) + if err != nil { + log.Fatalln("from:", err) + } + if len(pubData.Pub) != 1 { + log.Fatal("from: expected single public key") + } + fromA = pubData.Pub[0].A + fromId = pubData.Id + switch fromA { + case sntrup761x25519.SNTRUP761X25519, mceliece6960119x25519.ClassicMcEliece6960119X25519: + fromPub = pubData.Pub[0].V + fromPub = fromPub[len(fromPub)-X25519KeyLen:] + default: + log.Fatal("from: unsupported algorithm") } + var prv *cm.AV + prv, err = findPrv(pubData.Id) + if err != nil { + log.Fatalln("from:", err) + } + if prv == nil { + log.Fatal("from: can not find corresponding private key") + } + fromPrv = prv.V[len(prv.V)-X25519KeyLen:] } - fdPrvR.Close() - var err error var cek []byte if *doDecrypt { { @@ -228,10 +305,6 @@ func main() { continue } case sntrup761x25519.SNTRUP761X25519HKDFBLAKE2b: - if len(prvs) == 0 { - log.Println(kemIdx, kem.A, "skipping because no private key specified") - continue - } if kem.Encap == nil { log.Fatalln("missing encap") } @@ -239,217 +312,321 @@ func main() { if len(kem.Encap) != scheme.CiphertextSize()+X25519KeyLen { log.Fatalln("invalid encap len") } - for _, prv := range prvs { - if prv.A != sntrup761x25519.SNTRUP761X25519 { + if len(kem.To) == 0 { + if assumeTo == nil { + log.Println(kemIdx, kem.A, + `skipping because no "to" specified`) continue } - if len(prv.V) != scheme.PrivateKeySize()+X25519KeyLen { - log.Fatalln("invalid private keys len") - } - var ourSNTRUP sntrup761kem.PrivateKey - ourSNTRUP, err = scheme.UnmarshalBinaryPrivateKey( - prv.V[:scheme.PrivateKeySize()], - ) - if err != nil { - log.Fatal(err) + kem.To = assumeTo + } + var prv *cm.AV + prv, err = findPrv(kem.To) + if err != nil { + log.Fatal(err) + } + if prv == nil { + log.Println(kemIdx, kem.A, + "skipping because no private key found") + continue + } + if prv.A != sntrup761x25519.SNTRUP761X25519 { + log.Fatalln(kemIdx, kem.A, "differing algorithm") + } + if len(prv.V) != scheme.PrivateKeySize()+X25519KeyLen { + log.Fatalln("invalid private keys len") + } + var from *sign.PubData + if kem.From != nil { + if bytes.Equal(kem.From, bytes.Repeat([]byte{0}, 32)) { + kem.From = assumeFrom } - x25519 := ecdh.X25519() - var ourX25519 *ecdh.PrivateKey - ourX25519, err = x25519.NewPrivateKey( - prv.V[scheme.PrivateKeySize():], - ) + from, err = findPub(kem.From) if err != nil { - log.Fatal(err) + log.Fatalln("from:", err) } - theirSNTRUP := kem.Encap[:scheme.CiphertextSize()] - var keySNTRUP []byte - keySNTRUP, err = scheme.Decapsulate(ourSNTRUP, theirSNTRUP) - if err != nil { - continue + if from == nil { + log.Fatalln(kemIdx, kem.A, "can not find public key") } - var theirX25519 *ecdh.PublicKey - theirX25519, err = x25519.NewPublicKey( - kem.Encap[scheme.CiphertextSize():], - ) + } + var ourSNTRUP sntrup761kem.PrivateKey + ourSNTRUP, err = scheme.UnmarshalBinaryPrivateKey( + prv.V[:scheme.PrivateKeySize()], + ) + if err != nil { + log.Fatal(err) + } + x25519 := ecdh.X25519() + var ourX25519 *ecdh.PrivateKey + ourX25519, err = x25519.NewPrivateKey( + prv.V[scheme.PrivateKeySize():], + ) + if err != nil { + log.Fatal(err) + } + theirSNTRUP := kem.Encap[:scheme.CiphertextSize()] + var keySNTRUP []byte + keySNTRUP, err = scheme.Decapsulate(ourSNTRUP, theirSNTRUP) + if err != nil { + log.Fatalln(kemIdx, kem.A, "decapsulate:", err) + } + var theirX25519 *ecdh.PublicKey + theirX25519, err = x25519.NewPublicKey( + kem.Encap[scheme.CiphertextSize():], + ) + if err != nil { + log.Fatal(err) + } + var keyX25519 []byte + keyX25519, err = ourX25519.ECDH(theirX25519) + if err != nil { + log.Fatal(err) + } + { + var ourSNTRUPPub []byte + ourSNTRUPPub, err = ourSNTRUP.Public().MarshalBinary() if err != nil { log.Fatal(err) } - var keyX25519 []byte - keyX25519, err = ourX25519.ECDH(theirX25519) + ctHash := blake2b.Sum512(kem.Encap) + pkHash := blake2b.Sum512(append( + ourSNTRUPPub, + ourX25519.PublicKey().Bytes()..., + )) + ikm := bytes.Join([][]byte{ + keySNTRUP[:], keyX25519, ctHash[:], pkHash[:], + }, []byte{}) + var prk []byte + prk, err = hkdf.Extract(blake2bHash, ikm, nil) if err != nil { log.Fatal(err) } - { - - var ourSNTRUPPub []byte - ourSNTRUPPub, err = ourSNTRUP.Public().MarshalBinary() + if from != nil { + fromPub = from.Pub[0].V + fromPub = fromPub[len(fromPub)-X25519KeyLen:] + theirX25519, err = x25519.NewPublicKey(fromPub) if err != nil { log.Fatal(err) } - ctHash := blake2b.Sum512(kem.Encap) - pkHash := blake2b.Sum512(append( - ourSNTRUPPub, - ourX25519.PublicKey().Bytes()..., - )) - ikm := bytes.Join([][]byte{ - keySNTRUP[:], keyX25519, ctHash[:], pkHash[:], - }, []byte{}) - var prk []byte - prk, err = hkdf.Extract(blake2bHash, ikm, nil) + keyX25519, err = ourX25519.ECDH(theirX25519) if err != nil { log.Fatal(err) } - var kek []byte - kek, err = hkdf.Expand( + prk, err = hkdf.Expand( blake2bHash, prk, - string(append( - []byte(cmenc.SNTRUP761X25519Info), - encrypted.Id[:]...)), - chacha20poly1305.KeySize, + cmenc.SNTRUP761X25519AuthInfo, + blake2b.Size, ) if err != nil { log.Fatal(err) } - var cekp []byte - cekp, err = chapoly.Unwrap(kek, kem.CEK) + ikm = bytes.Join([][]byte{ + keyX25519, fromPub, ourX25519.PublicKey().Bytes(), + }, []byte{}) + prk, err = hkdf.Extract(blake2bHash, ikm, prk) if err != nil { - log.Println(kemIdx, kem.A, err, ", skipping") - continue - } - if len(cekp) != chapoly.CEKLen { - log.Println(kemIdx, kem.A, "wrong key len, skipping") - continue + log.Fatal(err) } - cek = cekp - break } + var kek []byte + kek, err = hkdf.Expand( + blake2bHash, + prk, + string(append( + []byte(cmenc.SNTRUP761X25519Info), + encrypted.Id[:]...)), + chacha20poly1305.KeySize) + if err != nil { + log.Fatal(err) + } + var cekp []byte + cekp, err = chapoly.Unwrap(kek, kem.CEK) + if err != nil { + log.Fatalln(kemIdx, kem.A, "unwrap:", err) + } + if len(cekp) != chapoly.CEKLen { + log.Fatalln(kemIdx, kem.A, "wrong key len, skipping") + } + cek = cekp + break } case mceliece6960119x25519.ClassicMcEliece6960119X25519HKDFSHAKE256: - if len(prvs) == 0 { - log.Println(kemIdx, kem.A, "skipping because no private key specified") - continue - } if kem.Encap == nil { log.Fatalln("missing encap") } if len(kem.Encap) != mceliece6960119.CiphertextSize+X25519KeyLen+chacha20poly1305.Overhead { log.Fatalln("invalid encap len") } - for _, prv := range prvs { - if prv.A != mceliece6960119x25519.ClassicMcEliece6960119X25519 { + if len(kem.To) == 0 { + if assumeTo == nil { + log.Println(kemIdx, kem.A, + `skipping because no "to" specified`) continue } - if len(prv.V) != mceliece6960119.PrivateKeySize+X25519KeyLen { - log.Fatalln("invalid private keys len") + kem.To = assumeTo + } + var prv *cm.AV + prv, err = findPrv(kem.To) + if err != nil { + log.Fatal(err) + } + if prv == nil { + log.Println(kemIdx, kem.A, + "skipping because no private key found") + continue + } + if prv.A != mceliece6960119x25519.ClassicMcEliece6960119X25519 { + continue + } + if len(prv.V) != mceliece6960119.PrivateKeySize+X25519KeyLen { + log.Fatalln("invalid private keys len") + } + var from *sign.PubData + if kem.From != nil { + if bytes.Equal(kem.From, bytes.Repeat([]byte{0}, 32)) { + kem.From = assumeFrom + } + from, err = findPub(kem.From) + if err != nil { + log.Fatalln("from:", err) + } + if from == nil { + log.Fatalln(kemIdx, kem.A, "can not find public key") } - var ourMcEliece *mceliece6960119.PrivateKey - ourMcEliece, err = mceliece6960119.UnmarshalBinaryPrivateKey( - prv.V[:len(prv.V)-X25519KeyLen], + } + var ourMcEliece *mceliece6960119.PrivateKey + ourMcEliece, err = mceliece6960119.UnmarshalBinaryPrivateKey( + prv.V[:len(prv.V)-X25519KeyLen], + ) + if err != nil { + log.Fatal(err) + } + x25519 := ecdh.X25519() + var ourX25519 *ecdh.PrivateKey + ourX25519, err = x25519.NewPrivateKey(prv.V[len(prv.V)-X25519KeyLen:]) + if err != nil { + log.Fatal(err) + } + theirMcEliece := (kem.Encap)[:len(kem.Encap)-X25519KeyLen-chacha20poly1305.Overhead] + theirX2559Encap := (kem.Encap)[len(kem.Encap)-X25519KeyLen-chacha20poly1305.Overhead:] + var keyMcEliece []byte + keyMcEliece, err = mceliece6960119.Decapsulate( + ourMcEliece, theirMcEliece, + ) + if err != nil { + log.Fatal(err) + } + var theirX25519 *ecdh.PublicKey + { + var decapKeymat []byte + decapKeymat, err = hkdf.Expand( + cmhash.NewSHAKE256, + keyMcEliece, + string(append( + []byte(cmenc.ClassicMcEliece6960119X25519DecapInfo), + encrypted.Id[:]...)), + chacha20poly1305.KeySize+chacha20poly1305.NonceSizeX, ) if err != nil { log.Fatal(err) } - x25519 := ecdh.X25519() - var ourX25519 *ecdh.PrivateKey - ourX25519, err = x25519.NewPrivateKey(prv.V[len(prv.V)-X25519KeyLen:]) + decapKey := decapKeymat[:chacha20poly1305.KeySize] + decapNonce := decapKeymat[chacha20poly1305.KeySize:] + var aead cipher.AEAD + aead, err = chacha20poly1305.NewX(decapKey) if err != nil { log.Fatal(err) } - theirMcEliece := (kem.Encap)[:len(kem.Encap)-X25519KeyLen-chacha20poly1305.Overhead] - theirX2559Encap := (kem.Encap)[len(kem.Encap)-X25519KeyLen-chacha20poly1305.Overhead:] - var keyMcEliece []byte - keyMcEliece, err = mceliece6960119.Decapsulate( - ourMcEliece, theirMcEliece, - ) + var theirX25519Raw []byte + theirX25519Raw, err = aead.Open( + nil, + decapNonce, + theirX2559Encap, + theirMcEliece) if err != nil { log.Fatal(err) } - var theirX25519 *ecdh.PublicKey - { - var decapKeymat []byte - decapKeymat, err = hkdf.Expand( - cmhash.NewSHAKE256, - keyMcEliece, - string(append( - []byte(cmenc.ClassicMcEliece6960119X25519DecapInfo), - encrypted.Id[:]...)), - chacha20poly1305.KeySize+chacha20poly1305.NonceSizeX, - ) - if err != nil { - log.Fatal(err) - } - decapKey := decapKeymat[:chacha20poly1305.KeySize] - decapNonce := decapKeymat[chacha20poly1305.KeySize:] - var aead cipher.AEAD - aead, err = chacha20poly1305.NewX(decapKey) - if err != nil { - log.Fatal(err) - } - var theirX25519Raw []byte - theirX25519Raw, err = aead.Open( - nil, - decapNonce, - theirX2559Encap, - theirMcEliece) - if err != nil { - log.Fatal(err) - } - theirX25519, err = x25519.NewPublicKey(theirX25519Raw) - if err != nil { - log.Fatal(err) - } + theirX25519, err = x25519.NewPublicKey(theirX25519Raw) + if err != nil { + log.Fatal(err) } - var keyX25519 []byte - keyX25519, err = ourX25519.ECDH(theirX25519) + } + var keyX25519 []byte + keyX25519, err = ourX25519.ECDH(theirX25519) + if err != nil { + log.Fatal(err) + } + { + ourMcEliecePub := ourMcEliece.Public() + var ourMcEliecePubRaw []byte + ourMcEliecePubRaw, err = ourMcEliecePub.MarshalBinary() if err != nil { log.Fatal(err) } - { - ourMcEliecePub := ourMcEliece.Public() - var ourMcEliecePubRaw []byte - ourMcEliecePubRaw, err = ourMcEliecePub.MarshalBinary() + pkHash := cmhash.NewSHAKE256() + pkHash.Write(ourMcEliecePubRaw) + pkHash.Write(ourX25519.PublicKey().Bytes()) + ikm := bytes.Join([][]byte{ + keyMcEliece, keyX25519, + sha3.SumSHAKE256(kem.Encap, 64), + pkHash.Sum(nil), + }, []byte{}) + var prk []byte + prk, err = hkdf.Extract(cmhash.NewSHAKE256, ikm, nil) + if err != nil { + log.Fatal(err) + } + if from != nil { + fromPub = from.Pub[0].V + fromPub = fromPub[len(fromPub)-X25519KeyLen:] + theirX25519, err = x25519.NewPublicKey(fromPub) if err != nil { log.Fatal(err) } - pkHash := cmhash.NewSHAKE256() - pkHash.Write(ourMcEliecePubRaw) - pkHash.Write(ourX25519.PublicKey().Bytes()) - ikm := bytes.Join([][]byte{ - keyMcEliece, keyX25519, - sha3.SumSHAKE256(kem.Encap, 64), - pkHash.Sum(nil), - }, []byte{}) - var prk []byte - prk, err = hkdf.Extract(cmhash.NewSHAKE256, ikm, nil) + keyX25519, err = ourX25519.ECDH(theirX25519) if err != nil { log.Fatal(err) } - var kek []byte - kek, err = hkdf.Expand( + prk, err = hkdf.Expand( cmhash.NewSHAKE256, prk, - string(append( - []byte(cmenc.ClassicMcEliece6960119X25519Info), - encrypted.Id[:]...)), - chacha20poly1305.KeySize, + cmenc.ClassicMcEliece6960119X25519AuthInfo, + 64, ) if err != nil { log.Fatal(err) } - var cekp []byte - cekp, err = chapoly.Unwrap(kek, kem.CEK) + ikm = bytes.Join([][]byte{ + keyX25519, fromPub, ourX25519.PublicKey().Bytes(), + }, []byte{}) + prk, err = hkdf.Extract(cmhash.NewSHAKE256, ikm, prk) if err != nil { - log.Println(kemIdx, kem.A, err, ", skipping") - continue - } - if len(cekp) != chapoly.CEKLen { - log.Println(kemIdx, kem.A, "wrong key len, skipping") - continue + log.Fatal(err) } - cek = cekp - break } + var kek []byte + kek, err = hkdf.Expand( + cmhash.NewSHAKE256, + prk, + string(append( + []byte(cmenc.ClassicMcEliece6960119X25519Info), + encrypted.Id[:]...)), + chacha20poly1305.KeySize, + ) + if err != nil { + log.Fatal(err) + } + var cekp []byte + cekp, err = chapoly.Unwrap(kek, kem.CEK) + if err != nil { + log.Fatalln(kemIdx, kem.A, "unwrap:", err) + } + if len(cekp) != chapoly.CEKLen { + log.Fatalln(kemIdx, kem.A, "wrong key len, skipping") + } + cek = cekp + break } default: log.Println("unsupported KEM:", kem.A) @@ -559,7 +736,7 @@ func main() { log.Fatal(err) } var ourPrvX25519 *ecdh.PrivateKey - ourPrvX25519, err = ecdh.X25519().GenerateKey(rand.Reader) + ourPrvX25519, err = x25519.GenerateKey(rand.Reader) if err != nil { log.Fatal(err) } @@ -584,6 +761,40 @@ func main() { if err != nil { log.Fatal(err) } + if fromPrv != nil { + if fromA != sntrup761x25519.SNTRUP761X25519 { + log.Fatal("differing sender/recipient algorithms") + } + ourPrvX25519, err = x25519.NewPrivateKey(fromPrv) + if err != nil { + log.Fatal(err) + } + keyX25519, err = ourPrvX25519.ECDH(theirX25519) + if err != nil { + log.Fatal(err) + } + prk, err = hkdf.Expand( + blake2bHash, + prk, + cmenc.SNTRUP761X25519AuthInfo, + blake2b.Size, + ) + if err != nil { + log.Fatal(err) + } + ikm = bytes.Join([][]byte{ + keyX25519, fromPub, theirX25519.Bytes(), + }, []byte{}) + prk, err = hkdf.Extract(blake2bHash, ikm, prk) + if err != nil { + log.Fatal(err) + } + if *noFrom { + kem.From = bytes.Repeat([]byte{0}, 32) + } else { + kem.From = fromId + } + } var kek []byte kek, err = hkdf.Expand( blake2bHash, @@ -601,7 +812,7 @@ func main() { } kem.CEK = cekp } - if *includeTo { + if !*noTo { kem.To = pubIds[pubId] } kems = append(kems, kem) @@ -679,6 +890,40 @@ func main() { if err != nil { log.Fatal(err) } + if fromPrv != nil { + if fromA != mceliece6960119x25519.ClassicMcEliece6960119X25519 { + log.Fatal("differing sender/recipient algorithms") + } + ourPrvX25519, err = x25519.NewPrivateKey(fromPrv) + if err != nil { + log.Fatal(err) + } + keyX25519, err = ourPrvX25519.ECDH(theirX25519) + if err != nil { + log.Fatal(err) + } + prk, err = hkdf.Expand( + cmhash.NewSHAKE256, + prk, + cmenc.ClassicMcEliece6960119X25519AuthInfo, + 64, + ) + if err != nil { + log.Fatal(err) + } + ikm = bytes.Join([][]byte{ + keyX25519, fromPub, theirX25519.Bytes(), + }, []byte{}) + prk, err = hkdf.Extract(cmhash.NewSHAKE256, ikm, prk) + if err != nil { + log.Fatal(err) + } + if *noFrom { + kem.From = bytes.Repeat([]byte{0}, 32) + } else { + kem.From = fromId + } + } var kek []byte kek, err = hkdf.Expand( cmhash.NewSHAKE256, @@ -698,7 +943,7 @@ func main() { } kem.CEK = cekp } - if *includeTo { + if !*noTo { kem.To = pubIds[pubId] } kems = append(kems, kem) diff --git a/go/cm/cmd/cmenctool/missing-from.t b/go/cm/cmd/cmenctool/missing-from.t new file mode 100755 index 0000000..d0fa9c7 --- /dev/null +++ b/go/cm/cmd/cmenctool/missing-from.t @@ -0,0 +1,30 @@ +#!/bin/sh + +test_description="Check workability with missing \"from\"" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +dd if=/dev/urandom of=enc.data bs=300K count=1 2>/dev/null +mkdir prvs pubs + +for algo in sntrup761-x25519 mceliece6960119-x25519 ; do + +test_expect_success "$algo: sender pub generation" "cmkeytool \ + -algo $algo -ku kem -sub N=S 5>s.pub 9>s.prv" +test_expect_success "$algo: recipient pub generation" "cmkeytool \ + -algo $algo -ku kem -sub N=R 5>r.pub 9>r.prv" +senderId=$(kekspp -v -p /data/id enc.enc" +test_expect_success "$algo: decrypting fails" " + ! cmenctool -d -prvs prvs -pubs pubs enc.data.got" +test_expect_success "$algo: decrypting" " + cmenctool -d -prvs prvs -pubs pubs -assume-from $senderId enc.data.got" +test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" + +done + +test_done diff --git a/go/cm/cmd/cmenctool/missing-to.t b/go/cm/cmd/cmenctool/missing-to.t new file mode 100755 index 0000000..38a27f1 --- /dev/null +++ b/go/cm/cmd/cmenctool/missing-to.t @@ -0,0 +1,23 @@ +#!/bin/sh + +test_description="Check workability with missing \"to\"" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +dd if=/dev/urandom of=enc.data bs=300K count=1 2>/dev/null + +for algo in sntrup761-x25519 mceliece6960119-x25519 ; do + +test_expect_success "$algo: pub generation" "cmkeytool \ + -algo sntrup761-x25519 -ku kem -sub N=0 5>enc.pub 9>enc.prv" +test_expect_success "$algo: encrypting" "cmenctool -no-to -to enc.pub enc.enc" +pubId=$(kekspp -v -p /data/id enc.data.got" +test_expect_success "$algo: decrypting" " + cmenctool -assume-to $pubId -d -prvs . enc.data.got" +test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" + +done + +test_done diff --git a/go/cm/cmd/cmenctool/multirecipient.t b/go/cm/cmd/cmenctool/multirecipient.t index a63e6e7..560da9c 100755 --- a/go/cm/cmd/cmenctool/multirecipient.t +++ b/go/cm/cmd/cmenctool/multirecipient.t @@ -11,14 +11,19 @@ test_expect_success "1: pub generation" "cmkeytool \ -algo sntrup761-x25519 -ku kem -sub N=1 5>enc.1.pub 9>enc.1.prv" test_expect_success "encrypting" " - cat enc.0.pub enc.1.pub | cmenctool 4<&0 enc.enc" + cmenctool -to enc.0.pub -to enc.1.pub enc.enc" +pubId=$(kekspp -v -p /data/id enc.data.got" + cmenctool -d -prvs . enc.data.got" test_expect_success "0: comparing" "test_cmp enc.data enc.data.got" +rm $pubId +pubId=$(kekspp -v -p /data/id enc.data.got" + cmenctool -d -prvs . enc.data.got" test_expect_success "1: comparing" "test_cmp enc.data enc.data.got" test_done diff --git a/go/cm/cmd/cmenctool/prv-encrypted.t b/go/cm/cmd/cmenctool/prv-encrypted.t index 8743185..f7c852b 100755 --- a/go/cm/cmd/cmenctool/prv-encrypted.t +++ b/go/cm/cmd/cmenctool/prv-encrypted.t @@ -3,16 +3,23 @@ test_description="Check passphrase-encrypted key decryption" . $SHARNESS_TEST_SRCDIR/sharness.sh -cmkeytool -algo sntrup761-x25519 -ku kem -sub A=KEY 5>enc.pub 9>enc.prv -dd if=/dev/urandom of=enc.data bs=12K count=1 2>/dev/null export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) +dd if=/dev/urandom of=enc.data bs=12K count=1 2>/dev/null balloonparams="-balloon-s 123 -balloon-t 2" -test_expect_success "key encrypting" " + +for algo in sntrup761-x25519 mceliece6960119-x25519 ; do + +test_expect_success "$algo: key generation" " + cmkeytool -algo $algo -ku kem -sub A=KEY 5>enc.pub 9>enc.prv" +test_expect_success "$algo: key encrypting" " cmenctool -p -embed $balloonparams enc.prv.enc" -test_expect_success "data encrypting" " - cmenctool 4enc.enc" -test_expect_success "decrypting" " - cmenctool -d 8enc.data.got" -test_expect_success "comparing" "test_cmp enc.data enc.data.got" +test_expect_success "$algo: data encrypting" " + cmenctool -to enc.pub enc.enc" +ln -s enc.prv.enc $(kekspp -v -p /data/id enc.data.got" +test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" + +done test_done diff --git a/go/cm/cmd/cmenctool/pub.t b/go/cm/cmd/cmenctool/pub.t index 4471ebf..ab24e3b 100755 --- a/go/cm/cmd/cmenctool/pub.t +++ b/go/cm/cmd/cmenctool/pub.t @@ -10,35 +10,39 @@ algo=mceliece6960119-x25519 algo0=$algo test_expect_success "$algo: pub generation" " cmkeytool -algo $algo -ku kem -sub A=$algo 5>enc.$algo.pub 9>enc.$algo.prv" +id0=$(kekspp -v -p /data/id enc.$algo.pub 9>enc.$algo.prv" +id1=$(kekspp -v -p /data/id enc.enc" + cmenctool -to enc.$algo0.pub -to enc.$algo1.pub enc.enc" +ln -s enc.$algo0.prv $id0 +ln -s enc.$algo1.prv $id1 test_expect_success "any: decrypting" " - cat enc.$algo0.prv enc.$algo1.prv | - cmenctool -d 8<&0 enc.data.got" + cmenctool -d -prvs . enc.data.got" test_expect_success "comparing" "test_cmp enc.data enc.data.got" +rm $id1 test_expect_success "$algo0: decrypting" " - cmenctool -d 8enc.data.got" + cmenctool -d -prvs . enc.data.got" test_expect_success "$algo0: comparing" "test_cmp enc.data enc.data.got" +rm $id0 +ln -s enc.$algo1.prv $id1 test_expect_success "$algo1: decrypting" " - cmenctool -d 8enc.data.got" + cmenctool -d -prvs . enc.data.got" +ln -s enc.$algo0.prv $id0 test_expect_success "$algo1: comparing" "test_cmp enc.data enc.data.got" export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) test_expect_success "encrypting also with passphrase" " - cat enc.$algo0.pub enc.$algo1.pub | - cmenctool $balloonparams -p 4<&0 enc.enc" + cmenctool $balloonparams -p -to enc.$algo0.pub -to enc.$algo1.pub enc.enc" test_expect_success "any: decrypting" " - cat enc.$algo0.prv enc.$algo1.prv | - cmenctool -d 8<&0 enc.data.got" + cmenctool -d -prvs . enc.data.got" test_expect_success "comparing" "test_cmp enc.data enc.data.got" test_expect_success "passphrase: decrypting" " cmenctool -d -p enc.data.got" diff --git a/go/cm/cmd/cmenctool/sender-auth.t b/go/cm/cmd/cmenctool/sender-auth.t new file mode 100755 index 0000000..c9079b3 --- /dev/null +++ b/go/cm/cmd/cmenctool/sender-auth.t @@ -0,0 +1,30 @@ +#!/bin/sh + +test_description="Sender authentication" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +dd if=/dev/urandom of=enc.data bs=300K count=1 2>/dev/null +mkdir prvs pubs + +for algo in sntrup761-x25519 mceliece6960119-x25519 ; do + +test_expect_success "$algo: sender pub generation" "cmkeytool \ + -algo $algo -ku kem -sub N=S 5>s.pub 9>s.prv" +test_expect_success "$algo: recipient pub generation" "cmkeytool \ + -algo $algo -ku kem -sub N=R 5>r.pub 9>r.prv" +ln -fs ../s.prv prvs/$(kekspp -v -p /data/id enc.enc" +test_expect_success "$algo: decrypting" " + cmenctool -d -prvs prvs -pubs pubs enc.data.got" +test_expect_success "$algo: comparing" "test_cmp enc.data enc.data.got" +ln -fs ../r.pub pubs/$(kekspp -v -p /data/id enc.data.got" + +done + +test_done diff --git a/go/cm/cmd/cmenctool/usage.go b/go/cm/cmd/cmenctool/usage.go index 937e5d5..c2d43d9 100644 --- a/go/cm/cmd/cmenctool/usage.go +++ b/go/cm/cmd/cmenctool/usage.go @@ -24,12 +24,17 @@ import ( func usage() { fmt.Fprintf(os.Stderr, `Usage: Encrypt to recipient(s): - cmenctool [-include-to] 4DATA.encrypted + cmenctool [-no-to] -to PUB0 [-to PUB1 ...] DATA.encrypted Encrypt on passphrase: cmenctool -p [-balloon-s X] [-balloon-t X] [-balloon-p X] DATA.encrypted Decrypt by providing possible KEMs and/or passphrase(s): - cmenctool -d [-p] 8DATA + cmenctool -d [-p] -prvs prvs/dir DATA + With sender authentication: + cmenctool [...] -from PUB -prvs prvs/dir [...] + cmenctool [...] -d -pubs pubs/dir [...] +{prvs,pubs}/dir must contain key files with uppercase hexadecimal +corresponding public key's id used as the filename. `) flag.PrintDefaults() } diff --git a/go/cm/enc/kem.go b/go/cm/enc/kem.go index 1d1521b..1a42145 100644 --- a/go/cm/enc/kem.go +++ b/go/cm/enc/kem.go @@ -5,16 +5,19 @@ import ( ) const ( - SNTRUP761X25519Info = "cm/encrypted/sntrup761-x25519-hkdf-blake2b" + SNTRUP761X25519Info = "cm/encrypted/sntrup761-x25519-hkdf-blake2b" + SNTRUP761X25519AuthInfo = "cm/encrypted/sntrup761-x25519-hkdf-blake2b/auth" ClassicMcEliece6960119X25519Info = "cm/encrypted/mceliece6960119-x25519-hkdf-shake256" + ClassicMcEliece6960119X25519AuthInfo = "cm/encrypted/mceliece6960119-x25519-hkdf-shake256/auth" ClassicMcEliece6960119X25519DecapInfo = "cm/encrypted/mceliece6960119-x25519-hkdf-shake256/decap" ) type KEM struct { - A string `keks:"a"` - CEK []byte `keks:"cek"` - To []byte `keks:"to,omitempty"` + A string `keks:"a"` + CEK []byte `keks:"cek"` + To []byte `keks:"to,omitempty"` + From []byte `keks:"from,omitempty"` // balloon-blake2b-hkdf related BalloonCost *balloon.Cost `keks:"cost,omitempty"` diff --git a/spec/cm/encrypted/authcrypt b/spec/cm/encrypted/authcrypt index 67034d8..b7d7ceb 100644 --- a/spec/cm/encrypted/authcrypt +++ b/spec/cm/encrypted/authcrypt @@ -8,8 +8,3 @@ sender's key that way -- implementation/protocol specific. Optional "/pubs" is a list public keys, which may be used to supply sender's public key(s). Public keys may be encrypted, to hide the actual deanonymisation contents. - -It is *highly* recommended to use multi-recipient safe DEM when -encrypting to multiple recipients. For example [cm/dem/xchacha-krmr] -instead of [cm/dem/xchapoly-krkc], but unfortunately with the price of -more expensive double pass authentication scheme. diff --git a/spec/cm/encrypted/index b/spec/cm/encrypted/index index d5bd6cf..d731976 100644 --- a/spec/cm/encrypted/index +++ b/spec/cm/encrypted/index @@ -40,6 +40,11 @@ Optional "/id" is used in KEMs for domain separation and envelope identification. UUIDv4 is recommended. If absent, then null UUID is used in KDF. +It is *highly* recommended to use multi-recipient safe DEM when +encrypting to multiple recipients. For example [cm/dem/xchacha-krmr] +instead of [cm/dem/xchapoly-krkc], but unfortunately with the price of +more expensive double pass authentication scheme. + [cm/encrypted/authcrypt] -- authenticated public-key encryption [cm/keywrap/] | key wrapping mechanisms [cm/dem/] | data encapsulation mechanisms diff --git a/spec/cm/kem/mceliece6960119-x25519-hkdf-shake256 b/spec/cm/kem/mceliece6960119-x25519-hkdf-shake256 index c0a2e90..86e05d6 100644 --- a/spec/cm/kem/mceliece6960119-x25519-hkdf-shake256 +++ b/spec/cm/kem/mceliece6960119-x25519-hkdf-shake256 @@ -23,11 +23,11 @@ Classic McEliece 6960-119 ciphertext, with XChaCha20-Poly1305-encrypted Recipient performs Classic McEliece decapsulation, decrypts ephemeral X25519 public key, computes shared secrets, combines them and derives KEK. - ==================================================== + ================================================ WARNING - ==================================================== - Sender authentication uses only *NON*-PQ crypto! - ==================================================== + ================================================ + Sender authentication uses only *NON*-PQ crypto! + ================================================ H = SHAKE256 mceliece-ciphertext, mceliece-shared-key = KEM-Encap(mceliece-recipient-public-key) @@ -46,8 +46,8 @@ X25519 public key, computes shared secrets, combines them and derives KEK. s-x25519-recipient-public-key) PRK = HKDF-Expand(H, prk=PRK, info="cm/encrypted/mceliece6960119-x25519-hkdf-shake256/auth") - PRK = HKDF-Extract(H, salt=PRK, ikm= - ss-x25519-shared-key || s-x25519-sender-public-key) + PRK = HKDF-Extract(H, salt=PRK, ikm= ss-x25519-shared-key || + s-x25519-sender-public-key || s-x25519-recipient-public-key) KEK = HKDF-Expand(H, prk=PRK, info="cm/encrypted/mceliece6960119-x25519-hkdf-shake256" || /id) diff --git a/spec/cm/kem/sntrup761-x25519-hkdf-blake2b b/spec/cm/kem/sntrup761-x25519-hkdf-blake2b index f9fc4c9..af4d837 100644 --- a/spec/cm/kem/sntrup761-x25519-hkdf-blake2b +++ b/spec/cm/kem/sntrup761-x25519-hkdf-blake2b @@ -18,11 +18,11 @@ 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. - ==================================================== + ================================================ WARNING - ==================================================== - Sender authentication uses only *NON*-PQ crypto! - ==================================================== + ================================================ + Sender authentication uses only *NON*-PQ crypto! + ================================================ H = BLAKE2b PRK = HKDF-Extract(H, salt="", ikm= @@ -30,12 +30,12 @@ key of the CEK. H(sntrup761-sender-ciphertext || e-x25519-sender-public-key) || H(sntrup761-recipient-public-key || s-x25519-recipient-public-key)) if {specified sender} + ss-x25519-shared-key = X25519(s-x25519-sender-private-key, + s-x25519-recipient-public-key) PRK = HKDF-Expand(H, prk=PRK, info="cm/encrypted/sntrup761-x25519-hkdf-blake2b/auth") - PRK = HKDF-Extract(H, salt=PRK, ikm= - ss-x25519-shared-key || - s-x25519-sender-public-key || - s-x25519-recipient-public-key) + PRK = HKDF-Extract(H, salt=PRK, ikm= ss-x25519-shared-key || + s-x25519-sender-public-key || s-x25519-recipient-public-key) KEK = HKDF-Expand(H, prk=PRK, info="cm/encrypted/sntrup761-x25519-hkdf-blake2b" || /id)