Skip to content

Add ML-KEM test vectors from CCTV#143

Merged
FiloSottile merged 3 commits intomasterfrom
push-tzooyulxntyk
Apr 5, 2025
Merged

Add ML-KEM test vectors from CCTV#143
FiloSottile merged 3 commits intomasterfrom
push-tzooyulxntyk

Conversation

@FiloSottile
Copy link
Member

Moved over the two sets that made sense. The unlucky one will be part of #110. The accumulated ones are blocked on #134. Also, used the short subset of the modulus overflow tests, as the full set uncompressed is in the megabytes (again, see #134).

@FiloSottile FiloSottile requested a review from cpu April 3, 2025 15:24
Copy link
Member

@cpu cpu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

Generator, for posterity.

package main

import (
	"crypto/rand"
	"fmt"

	kem "github.com/cloudflare/circl/kem/mlkem/mlkem512"    // or -768, or -1024
	cpapke "github.com/cloudflare/circl/pke/kyber/kyber512" // or -768, or -1024
	"golang.org/x/crypto/sha3"
)

func main() {
	seed := make([]byte, 64)
	rand.Read(seed)
	fmt.Printf("seed: %x\n", seed)

	pk, sk := cpapke.NewKeyFromSeedMLKEM(seed[:cpapke.KeySeedSize])
	ppk := make([]byte, cpapke.PublicKeySize)
	pk.Pack(ppk)
	fmt.Printf("ek: %x\n", ppk)
	hpk := sha3.Sum256(ppk)

	for {
		ct := make([]byte, cpapke.CiphertextSize)
		rand.Read(ct[1:])

		m := make([]byte, 32)
		sk.DecryptTo(m, ct)

		g := sha3.New512()
		g.Write(m)
		g.Write(hpk[:])
		j := g.Sum(nil)

		ct2 := make([]byte, cpapke.CiphertextSize)
		pk.EncryptTo(ct2, m, j[32:])

		if ct2[0] == 0 {
			fmt.Printf("ct: %x\n", ct)

			_, sk := kem.NewKeyFromSeed(seed)
			K := make([]byte, 32)
			sk.DecapsulateTo(K, ct)
			fmt.Printf("K: %x\n", K)

			break
		}
	}
}

-- go.mod --
module c2sp.org/CCTV/ML-KEM/strcmp

go 1.24.1

require (
	github.com/cloudflare/circl v1.6.0
	golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d
)

require golang.org/x/sys v0.10.0 // indirect
Generator, for posterity.

package main

import (
	"flag"
	"fmt"
	"math/rand"
)

const (
	n = 256
	q = 3329

	encodingSize12 = n * 12 / 8
)

var shortFlag = flag.Bool("short", false, "generate the short subset")
var kFlag = flag.Int("k", 3, "2 for -512, 3 for -768, 4 for -1024")
var startID = flag.Int("start", 1, "start ID for the first vector")

func main() {
	flag.Parse()
	for i := 0; i < *kFlag; i++ {
		genVector(func(t [][n]uint16) { t[i][0] = q })
		genVector(func(t [][n]uint16) { t[i][255] = q })
		genVector(func(t [][n]uint16) { t[i][0] = 1<<12 - 1 })
		genVector(func(t [][n]uint16) { t[i][255] = 1<<12 - 1 })
	}
	if !*shortFlag {
		var i, j int
		var x uint16 = q
		var doneValues, donePositions bool
		for {
			genVector(func(t [][n]uint16) { t[i][j] = x })
			x++
			if x == 1<<12 {
				x = q
				doneValues = true
			}
			j++
			if j == n {
				j = 0
				i++
			}
			if i == *kFlag {
				i = 0
				donePositions = true
			}
			if doneValues && donePositions {
				break
			}
		}
	}
}

const template = `{
    "tcId": %d,
    "flags": [
        "ModulusOverflow"
    ],
    "m": "42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242",
    "ek": "%x",
    "c": "",
    "K": "",
    "result": "invalid"
},
`

func genVector(f func([][n]uint16)) {
	t := make([][n]uint16, *kFlag)
	for i := range t {
		t[i] = r[i]
	}
	f(t)
	out := make([]byte, 0)
	for i := range t {
		out = polyByteEncode(out, t[i])
	}
	out = append(out, ρ...)
	fmt.Printf(template, *startID, out)
	*startID++
}

var r = [4][n]uint16{randomPoly(), randomPoly(), randomPoly(), randomPoly()}
var ρ = make([]byte, 32)

func init() { rand.Read(ρ) }

func randomPoly() [n]uint16 {
	var f [n]uint16
	for i := range f {
		f[i] = uint16(rand.Intn(q))
	}
	return f
}

// polyByteEncode appends the 384-byte encoding of f to b.
//
// It implements ByteEncode₁₂, according to FIPS 203 (DRAFT), Algorithm 4.
func polyByteEncode(b []byte, f [n]uint16) []byte {
	out, B := sliceForAppend(b, encodingSize12)
	for i := 0; i < n; i += 2 {
		x := uint32(f[i]) | uint32(f[i+1])<<12
		B[0] = uint8(x)
		B[1] = uint8(x >> 8)
		B[2] = uint8(x >> 16)
		B = B[3:]
	}
	return out
}

// sliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
func sliceForAppend(in []byte, n int) (head, tail []byte) {
	if total := len(in) + n; cap(in) >= total {
		head = in[:total]
	} else {
		head = make([]byte, total)
		copy(head, in)
	}
	tail = head[len(in):]
	return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants