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

ascon: update formulas and check for API compatibility #406

Merged
merged 2 commits into from Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
164 changes: 87 additions & 77 deletions cipher/ascon/ascon.go
@@ -1,8 +1,14 @@
// Package ascon provides a light-weight AEAD cipher.
// Package ascon provides ASCON family of light-weight AEAD ciphers.
//
// This package implements Ascon128 and Ascon128a two AEAD ciphers as specified
// in ASCON v1.2 by C. Dobraunig, M. Eichlseder, F. Mendel, M. Schläffer.
// https://ascon.iaik.tugraz.at/index.html
//
// It also implements Ascon-80pq, which has an increased key-size to provide
// more resistance against a quantum adversary using Grover’s algorithm for
// key search. Since Ascon-128 and Ascon-80pq share the same building blocks
// and same parameters except the size of the key, it is claimed the same
// security for Ascon-80pq against classical attacks as for Ascon-128.
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't Ascon80pq provide 160b of classical security?

Copy link
Member

Choose a reason for hiding this comment

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

I think it's good to add although Ascon-128 already reaches approximately NIST level 1 post-quantum security.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@meichlseder can give us more details.

Choose a reason for hiding this comment

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

The text is correct, we don't claim 160 bits of classical security for Ascon-80pq.
Several parts of Ascon are scaled for 128-bit security (with data limit 2^64), including the sponge capacity, tag size, permutation distinguisher bounds, etc., thus the common 128-bit security claim for all variants.
Depending on your precise definition of b-bit security and additional constraints besides offline time complexity (data limits including number of encryption/decryption queries, misuse settings, etc), ymmv.

package ascon

import (
Expand All @@ -13,14 +19,25 @@ import (
)

const (
KeySize = 16
KeySize80pq = 20
KeySize = 16 // For Ascon128 and Ascon128a.
KeySize80pq = 20 // Only for Ascon80pq.
NonceSize = 16
TagSize = KeySize
TagSize = 16
)

type Mode int

// KeySize is 16 for Ascon128 and Ascon128a, or 20 for Ascon80pq.
func (m Mode) KeySize() int {
switch m {
case Ascon128, Ascon128a, Ascon80pq:
v := int(m) >> 2
return KeySize&^v | KeySize80pq&v
default:
panic(ErrMode)
}
}

func (m Mode) String() string {
switch m {
case Ascon128:
Expand All @@ -35,31 +52,22 @@ func (m Mode) String() string {
}

const (
Ascon128 Mode = iota + 1
Ascon128a
// Ascon-80pq has an increased key-size to provide more resistance against a quantum
// adversary using Grover’s algorithm for key search. Since Ascon-128 and Ascon-
// 80pq share the same building blocks and same parameters except the size of the key,
// we claim the same security for Ascon-80pq against classical attacks as for Ascon-128.
Ascon80pq
Ascon128 Mode = 1
Ascon128a Mode = 2
Ascon80pq Mode = -1
)

const (
permA = 12
permB = 6 // 6 for Ascon128 and Ascon80pq, or 8 for Ascon128a
blockSize = 8 // 8 for Ascon128 and Ascon80pq, or 16 for Ascon128a
ivSize = 8
stateSize = ivSize + KeySize + NonceSize
)
const permA = 12

type Cipher struct {
s [5]uint64
key [3]uint64
mode Mode
}

// New returns a Cipher struct implementing the cipher.AEAD interface. Mode is
// one of Ascon128, Ascon128a or Ascon80pq.
// New returns a Cipher struct implementing the crypto/cipher.AEAD interface.
// The key must be Mode.KeySize() bytes long, and the mode is one of Ascon128,
// Ascon128a or Ascon80pq.
func New(key []byte, m Mode) (*Cipher, error) {
if (m == Ascon128 || m == Ascon128a) && len(key) != KeySize {
return nil, ErrKeySize
Expand All @@ -73,15 +81,15 @@ func New(key []byte, m Mode) (*Cipher, error) {
c := new(Cipher)
c.mode = m
if m == Ascon80pq {
c.key[0] = (uint64)(binary.BigEndian.Uint32(key[0:4]))
c.key[0] = uint64(binary.BigEndian.Uint32(key[0:4]))
c.key[1] = binary.BigEndian.Uint64(key[4:12])
c.key[2] = binary.BigEndian.Uint64(key[12:20])
return c, nil
} else {
c.key[0] = 0
c.key[1] = binary.BigEndian.Uint64(key[0:8])
c.key[2] = binary.BigEndian.Uint64(key[8:16])
}

c.key[0] = binary.BigEndian.Uint64(key[0:8])
c.key[1] = binary.BigEndian.Uint64(key[8:16])

return c, nil
}

Expand All @@ -106,15 +114,15 @@ func (a *Cipher) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
}

ptLen := len(plaintext)
output := make([]byte, ptLen+TagSize)
ciphertext, tag := output[:ptLen], output[ptLen:]
ret, out := sliceForAppend(dst, ptLen+TagSize)
ciphertext, tag := out[:ptLen], out[ptLen:]

a.initialize(nonce)
a.assocData(additionalData)
a.procText(plaintext, ciphertext, true)
a.finalize(tag)

return output
return ret
}

// Open decrypts and authenticates ciphertext, authenticates the
Expand All @@ -137,7 +145,8 @@ func (a *Cipher) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, er
}

ptLen := len(ciphertext) - TagSize
plaintext := make([]byte, ptLen)
ret, out := sliceForAppend(dst, ptLen)
plaintext := out[:ptLen]
ciphertext, tag0 := ciphertext[:ptLen], ciphertext[ptLen:]
tag1 := (&[TagSize]byte{})[:]

Expand All @@ -150,46 +159,38 @@ func (a *Cipher) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, er
return nil, ErrDecryption
}

return plaintext, nil
return ret, nil
}

func abs(x int) int { m := uint(x >> (bits.UintSize - 1)); return int((uint(x) + m) ^ m) }

// blockSize = 8 for Ascon128 and Ascon80pq, or 16 for Ascon128a.
func (a *Cipher) blockSize() int { return abs(int(a.mode)) << 3 }

// permB = 6 for Ascon128 and Ascon80pq, or 8 for Ascon128a.
func (a *Cipher) permB() int { return (abs(int(a.mode)) + 2) << 1 }

func (a *Cipher) initialize(nonce []byte) {
if a.mode == Ascon80pq {
bcs := blockSize * uint64(Ascon128)
pB := permB + 2*(uint64(Ascon128)-1)
a.s[0] = ((KeySize80pq * 8) << 56) | ((bcs * 8) << 48) | (permA << 40) | (pB << 32) | a.key[0]
a.s[1] = a.key[1]
a.s[2] = a.key[2]
} else {
bcs := blockSize * uint64(a.mode)
pB := permB + 2*(uint64(a.mode)-1)
a.s[0] = ((KeySize * 8) << 56) | ((bcs * 8) << 48) | (permA << 40) | (pB << 32)
a.s[1] = a.key[0]
a.s[2] = a.key[1]
}
bcs := uint64(a.blockSize())
pB := uint64(a.permB())
kS := uint64(a.mode.KeySize())

a.s[0] = ((kS * 8) << 56) | ((bcs * 8) << 48) | (permA << 40) | (pB << 32) | a.key[0]
a.s[1] = a.key[1]
a.s[2] = a.key[2]
a.s[3] = binary.BigEndian.Uint64(nonce[0:8])
a.s[4] = binary.BigEndian.Uint64(nonce[8:16])

a.perm(permA)

if a.mode == Ascon80pq {
a.s[2] ^= a.key[0]
a.s[3] ^= a.key[1]
a.s[4] ^= a.key[2]
} else {
a.s[3] ^= a.key[0]
a.s[4] ^= a.key[1]
}
a.s[2] ^= a.key[0]
a.s[3] ^= a.key[1]
a.s[4] ^= a.key[2]
}

func (a *Cipher) assocData(add []byte) {
m := a.mode
if m == Ascon80pq {
m = Ascon128
}

bcs := blockSize * int(m)
pB := permB + 2*(int(m)-1)
bcs := a.blockSize()
pB := a.permB()
if len(add) > 0 {
for ; len(add) >= bcs; add = add[bcs:] {
for i := 0; i < bcs; i += 8 {
Expand All @@ -207,13 +208,8 @@ func (a *Cipher) assocData(add []byte) {
}

func (a *Cipher) procText(in, out []byte, enc bool) {
m := a.mode
if m == Ascon80pq {
m = Ascon128
}

bcs := blockSize * int(m)
pB := permB + 2*(int(m)-1)
bcs := a.blockSize()
pB := a.permB()
mask := uint64(0)
if enc {
mask -= 1
Expand All @@ -234,30 +230,29 @@ func (a *Cipher) procText(in, out []byte, enc bool) {
for i := 0; i < len(in); i++ {
off := 56 - (8 * (i % 8))
si := byte((a.s[i/8] >> off) & 0xFF)
out[i] = si ^ in[i]
ss := (in[i] &^ mask8) | (out[i] & mask8)
inB := in[i]
outB := si ^ inB
out[i] = outB
ss := inB&^mask8 | outB&mask8
a.s[i/8] = (a.s[i/8] &^ (0xFF << off)) | uint64(ss)<<off
}
a.s[len(in)/8] ^= uint64(0x80) << (56 - 8*(len(in)%8))
}

func (a *Cipher) finalize(tag []byte) {
bcs := a.blockSize()
if a.mode == Ascon80pq {
bcs := blockSize * int(Ascon128)
a.s[bcs/8+0] ^= a.key[0]<<32 | a.key[1]>>32
a.s[bcs/8+1] ^= a.key[1]<<32 | a.key[2]>>32
a.s[bcs/8+2] ^= a.key[2] << 32
a.perm(permA)
binary.BigEndian.PutUint64(tag[0:8], a.s[3]^a.key[1])
binary.BigEndian.PutUint64(tag[8:16], a.s[4]^a.key[2])
} else {
bcs := blockSize * int(a.mode)
a.s[bcs/8+0] ^= a.key[0]
a.s[bcs/8+1] ^= a.key[1]
a.perm(permA)
binary.BigEndian.PutUint64(tag[0:8], a.s[3]^a.key[0])
binary.BigEndian.PutUint64(tag[8:16], a.s[4]^a.key[1])
a.s[bcs/8+0] ^= a.key[1]
a.s[bcs/8+1] ^= a.key[2]
}

a.perm(permA)
binary.BigEndian.PutUint64(tag[0:8], a.s[3]^a.key[1])
binary.BigEndian.PutUint64(tag[8:16], a.s[4]^a.key[2])
}

var roundConst = [12]uint64{0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b}
Expand Down Expand Up @@ -304,6 +299,21 @@ func (a *Cipher) perm(n int) {
a.s[0], a.s[1], a.s[2], a.s[3], a.s[4] = x0, x1, x2, x3, x4
}

// 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
}

var (
ErrKeySize = errors.New("ascon: bad key size")
ErrNonceSize = errors.New("ascon: bad nonce size")
Expand Down
56 changes: 56 additions & 0 deletions cipher/ascon/ascon_test.go
Expand Up @@ -127,6 +127,62 @@ func TestBadInputs(t *testing.T) {
test.CheckIsErr(t, err, "should panic due to bad ciphertext")
}

func TestAPI(t *testing.T) {
key, _ := hexa.DecodeString("000102030405060708090A0B0C0D0E0F")
nonce, _ := hexa.DecodeString("000102030405060708090A0B0C0D0E0F")
c, _ := ascon.New(key, ascon.Ascon128)
pt := []byte("helloworld")
ct, _ := hexa.DecodeString("d4e663d29cd60a693c20f890982e167d266f940b93b586945065")

t.Run("append", func(t *testing.T) {
prefix := [5]byte{0x1F, 0x2F, 0x3F, 0x4F, 0x5F}
prefixAndCt := c.Seal(prefix[:], nonce, pt, nil)
got := prefixAndCt
want := append(append([]byte{}, prefix[:]...), ct...)
if !bytes.Equal(got, want) {
test.ReportError(t, got, want)
}

ciphertext := prefixAndCt[len(prefix):]
prefix = [5]byte{0x11, 0x22, 0x33, 0x44, 0x55}
prefixAndPt, err := c.Open(prefix[:], nonce, ciphertext, nil)
if err != nil {
t.Fatal(err)
}
got = prefixAndPt
want = append(append([]byte{}, prefix[:]...), pt...)
if !bytes.Equal(got, want) {
test.ReportError(t, got, want)
}
})
t.Run("reuse", func(t *testing.T) {
ptWithCap := make([]byte, len(pt), len(pt)+100)
copy(ptWithCap, pt)
// reusing the input to store the ciphertext.
ciphertext := c.Seal(ptWithCap[:0], nonce, ptWithCap, nil)
got := ciphertext
want := ct
if !bytes.Equal(got, want) {
test.ReportError(t, got, want)
}
test.CheckOk(&ptWithCap[0] == &ciphertext[0], "should have same address", t)

ctWithCap := make([]byte, len(ct), len(ct)+100)
copy(ctWithCap, ct)
// reusing the input to store the plaintext.
plaintext, err := c.Open(ctWithCap[:0], nonce, ctWithCap, nil)
if err != nil {
t.Fatal(err)
}
got = plaintext
want = pt
if !bytes.Equal(got, want) {
test.ReportError(t, got, want)
}
test.CheckOk(&ctWithCap[0] == &plaintext[0], "should have same address", t)
})
}

func BenchmarkAscon(b *testing.B) {
for _, mode := range []ascon.Mode{ascon.Ascon128, ascon.Ascon128a, ascon.Ascon80pq} {
for _, length := range []int{64, 1350, 8 * 1024} {
Expand Down