Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MAYO #483

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions sign/mayo/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:generate go run gen.go

// Package mayo implements the MAYO signature scheme
// as submitted to round1 of the NIST PQC competition of Additional Signature Scehemes and described in
//
// https://csrc.nist.gov/csrc/media/Projects/pqc-dig-sig/documents/round-1/spec-files/mayo-spec-web.pdf
//
// This implemented the nibble-sliced version as proposed in
//
// https://eprint.iacr.org/2023/1683
package mayo
233 changes: 233 additions & 0 deletions sign/mayo/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//go:build ignore
// +build ignore

// Autogenerates wrappers from templates to prevent too much duplicated code
// between the code for different modes.
package main

import (
"bytes"
"fmt"
"go/format"
"os"
"path"
"strings"
"text/template"
)

type Mode struct {
Name string
N int
M int
O int
K int
KeySeedSize int
DigestSize int
Tail [5]uint8
}

func (m Mode) Pkg() string {
return strings.ToLower(m.Mode())
}

func (m Mode) Impl() string {
return "impl" + m.Mode()
}

func (m Mode) Mode() string {
return strings.ReplaceAll(m.Name, "MAYO_", "Mode")
}

var (
Modes = []Mode{
{
Name: "MAYO_1",
N: 66,
M: 64,
O: 8,
K: 9,
KeySeedSize: 24,
DigestSize: 32,
Tail: [5]uint8{8, 0, 2, 8, 0},
},
{
Name: "MAYO_2",
N: 78,
M: 64,
O: 18,
K: 4,
KeySeedSize: 24,
DigestSize: 32,
Tail: [5]uint8{8, 0, 2, 8, 0},
},
{
Name: "MAYO_3",
N: 99,
M: 96,
O: 10,
K: 11,
KeySeedSize: 32,
DigestSize: 48,
Tail: [5]uint8{2, 2, 0, 2, 0},
},
{
Name: "MAYO_5",
N: 133,
M: 128,
O: 12,
K: 12,
KeySeedSize: 40,
DigestSize: 64,
Tail: [5]uint8{4, 8, 0, 4, 2},
},
}
TemplateWarning = "// Code generated from"
)

func main() {
generateModePackageFiles()
generateParamsFiles()
generateSourceFiles()
}

// Generates modeX/internal/params.go from templates/params.templ.go
func generateParamsFiles() {
tl, err := template.ParseFiles("templates/params.templ.go")
if err != nil {
panic(err)
}

for _, mode := range Modes {
buf := new(bytes.Buffer)
err := tl.Execute(buf, mode)
if err != nil {
panic(err)
}

// Formating output code
code, err := format.Source(buf.Bytes())
if err != nil {
fmt.Println(buf.String())
panic("error formating code")
}

res := string(code)
offset := strings.Index(res, TemplateWarning)
if offset == -1 {
panic("Missing template warning in params.templ.go")
}
err = os.WriteFile(mode.Pkg()+"/internal/params.go",
[]byte(res[offset:]), 0o644)
if err != nil {
panic(err)
}
}
}

// Generates modeX/mayo.go from templates/modePkg.templ.go
func generateModePackageFiles() {
tl, err := template.ParseFiles("templates/modePkg.templ.go")
if err != nil {
panic(err)
}

for _, mode := range Modes {
buf := new(bytes.Buffer)
err := tl.Execute(buf, mode)
if err != nil {
panic(err)
}

res := buf.String()
offset := strings.Index(res, TemplateWarning)
if offset == -1 {
panic("Missing template warning in modePkg.templ.go")
}
err = os.WriteFile(mode.Pkg()+"/mayo.go", []byte(res[offset:]), 0o644)
if err != nil {
panic(err)
}
}
}

// Copies mode1 source files to other modes
func generateSourceFiles() {
files := make(map[string][]byte)

// Ignore mode specific files.
ignored := func(x string) bool {
return x == "params.go" || x == "params_test.go" ||
strings.HasSuffix(x, ".swp")
}

fs, err := os.ReadDir("mode1/internal")
if err != nil {
panic(err)
}

// Read files
for _, f := range fs {
name := f.Name()
if ignored(name) {
continue
}
files[name], err = os.ReadFile(path.Join("mode1/internal", name))
if err != nil {
panic(err)
}
}

// Go over modes
for _, mode := range Modes {
if mode.Name == "MAYO_1" {
continue
}

fs, _ = os.ReadDir(path.Join(mode.Pkg(), "internal"))
for _, f := range fs {
name := f.Name()
fn := path.Join(mode.Pkg(), "internal", name)
if ignored(name) {
continue
}
_, ok := files[name]
if !ok {
fmt.Printf("Removing superfluous file: %s\n", fn)
err = os.Remove(fn)
if err != nil {
panic(err)
}
}
if f.IsDir() {
panic(fmt.Sprintf("%s: is a directory", fn))
}
if f.Type()&os.ModeSymlink != 0 {
fmt.Printf("Removing symlink: %s\n", fn)
err = os.Remove(fn)
if err != nil {
panic(err)
}
}
}
for name, expected := range files {
fn := path.Join(mode.Pkg(), "internal", name)
expected = []byte(fmt.Sprintf(
"%s mode1/internal/%s by gen.go\n\n%s",
TemplateWarning,
name,
string(expected),
))
got, err := os.ReadFile(fn)
if err == nil {
if bytes.Equal(got, expected) {
continue
}
}
fmt.Printf("Updating %s\n", fn)
err = os.WriteFile(fn, expected, 0o644)
if err != nil {
panic(err)
}
}
}
}
71 changes: 71 additions & 0 deletions sign/mayo/internal/common/nist/drbg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Package nist implements helpers to generate NIST's Known Answer Tests (KATs).
Copy link
Member

Choose a reason for hiding this comment

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

Why not use github.com/cloudflare/circl/internal/nist?

Copy link
Author

Choose a reason for hiding this comment

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

github.com/cloudflare/circl/internal/nist does not implement io.Reader, which was somehow used during the debug process when implementing MAYO. Can change back to this one.

Copy link
Author

@ilway25 ilway25 Mar 5, 2024

Choose a reason for hiding this comment

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

Please ignore the previous comment. I think it would be better if github.com/cloudflare/circl/internal/nist implements io.Reader because MAYO's sign takes a random source, which is also used during KAT tests.

package nist

import (
"crypto/aes"
"io"
)

// See NIST's PQCgenKAT.c.
type DRBG struct {
key [32]byte
v [16]byte

io.Reader
}

func (g *DRBG) incV() {
for j := 15; j >= 0; j-- {
if g.v[j] == 255 {
g.v[j] = 0
} else {
g.v[j]++
break
}
}
}

// AES256_CTR_DRBG_Update(pd, &g.key, &g.v).
func (g *DRBG) update(pd *[48]byte) {
var buf [48]byte
b, _ := aes.NewCipher(g.key[:])
for i := 0; i < 3; i++ {
g.incV()
b.Encrypt(buf[i*16:(i+1)*16], g.v[:])
}
if pd != nil {
for i := 0; i < 48; i++ {
buf[i] ^= pd[i]
}
}
copy(g.key[:], buf[:32])
copy(g.v[:], buf[32:])
}

// randombyte_init(seed, NULL, 256).
func NewDRBG(seed *[48]byte) (g DRBG) {
g.update(seed)
return
}

// randombytes.
func (g *DRBG) Read(x []byte) (n int, err error) {
var block [16]byte

n, err = len(x), nil

b, _ := aes.NewCipher(g.key[:])
for len(x) > 0 {
g.incV()
b.Encrypt(block[:], g.v[:])
if len(x) < 16 {
copy(x[:], block[:len(x)])
break
}
copy(x[:], block[:])
x = x[16:]
}
g.update(nil)

return
}