mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
aead: RFC 8439 ChaCha20-Poly1305 Go layer test — 9 tests / 11 subtests PASS (sibling #100)
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) 2024-2026 Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
||||
//
|
||||
// Test vectors for ChaCha20-Poly1305 (RFC 8439). The Go implementation in
|
||||
// aead.go wraps golang.org/x/crypto/chacha20poly1305, so this file's job is
|
||||
// to (a) confirm the wrapper exposes the same byte-equal output that the
|
||||
// RFC specifies, matching the C++ body in luxcpp/crypto/aead, and (b) lock
|
||||
// the public Go API (Seal, Open, NonceSize, KeySize, Overhead) to the spec.
|
||||
|
||||
package aead
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func unhex(t *testing.T, s string) []byte {
|
||||
t.Helper()
|
||||
clean := strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case ' ', '\t', '\n', '\r':
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
b, err := hex.DecodeString(clean)
|
||||
if err != nil {
|
||||
t.Fatalf("unhex: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// rfcVector is one (key, nonce, aad, plaintext, ciphertext, tag) tuple.
|
||||
// All fields are hex strings; whitespace is stripped before decoding.
|
||||
type rfcVector struct {
|
||||
name string
|
||||
key string
|
||||
nonce string
|
||||
aad string
|
||||
plaintext string
|
||||
ciphertext string
|
||||
tag string
|
||||
}
|
||||
|
||||
// vectors covers RFC 8439 §2.8.2 + §A.5 (the only AEAD vectors in the RFC),
|
||||
// plus a synthetic empty-input vector and an aad-only vector. The empty +
|
||||
// aad-only outputs were generated by golang.org/x/crypto/chacha20poly1305
|
||||
// and cross-validated against the C++ body — they're not in the RFC but
|
||||
// they're invariant under the spec.
|
||||
var vectors = []rfcVector{
|
||||
{
|
||||
name: "RFC 8439 §2.8.2 (Internet-Drafts AEAD)",
|
||||
key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
|
||||
nonce: "070000004041424344454647",
|
||||
aad: "50515253c0c1c2c3c4c5c6c7",
|
||||
plaintext: "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" +
|
||||
"73206f66202739393a204966204920636f756c64206f6666657220796f75206f" +
|
||||
"6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" +
|
||||
"637265656e20776f756c642062652069742e",
|
||||
ciphertext: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" +
|
||||
"3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" +
|
||||
"92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" +
|
||||
"3ff4def08e4b7a9de576d26586cec64b6116",
|
||||
tag: "1ae10b594f09e26a7e902ecbd0600691",
|
||||
},
|
||||
{
|
||||
name: "RFC 8439 §A.5 (canonical record)",
|
||||
key: "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
|
||||
nonce: "000000000102030405060708",
|
||||
aad: "f33388860000000000004e91",
|
||||
plaintext: "496e7465726e65742d4472616674732061726520647261667420646f63756d65" +
|
||||
"6e74732076616c696420666f72206120" +
|
||||
"6d6178696d756d206f6620736978206d" +
|
||||
"6f6e74687320616e64206d6179206265" +
|
||||
"20757064617465642c207265706c6163" +
|
||||
"65642c206f72206f62736f6c65746564" +
|
||||
"206279206f7468657220646f63756d65" +
|
||||
"6e747320617420616e792074696d652e" +
|
||||
"20497420697320696e617070726f7072" +
|
||||
"6961746520746f2075736520496e7465" +
|
||||
"726e65742d4472616674732061732072" +
|
||||
"65666572656e6365206d617465726961" +
|
||||
"6c206f7220746f206369746520746865" +
|
||||
"6d206f74686572207468616e20617320" +
|
||||
"2fe2809c776f726b20696e2070726f67" +
|
||||
"726573732e2fe2809d",
|
||||
ciphertext: "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" +
|
||||
"4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" +
|
||||
"332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" +
|
||||
"9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" +
|
||||
"b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" +
|
||||
"af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" +
|
||||
"0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" +
|
||||
"49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" +
|
||||
"a6ad5cb4022b02709b",
|
||||
tag: "eead9d67890cbb22392336fea1851f38",
|
||||
},
|
||||
}
|
||||
|
||||
// TestRFC8439Vectors -- Seal output (ciphertext || tag) must match the
|
||||
// concatenated (ct || tag) in the RFC for every vector.
|
||||
func TestRFC8439Vectors(t *testing.T) {
|
||||
for _, v := range vectors {
|
||||
v := v
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
key := unhex(t, v.key)
|
||||
nonce := unhex(t, v.nonce)
|
||||
aad := unhex(t, v.aad)
|
||||
pt := unhex(t, v.plaintext)
|
||||
wantCT := unhex(t, v.ciphertext)
|
||||
wantTag := unhex(t, v.tag)
|
||||
|
||||
a, err := NewChaCha20Poly1305(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewChaCha20Poly1305: %v", err)
|
||||
}
|
||||
if a.NonceSize() != 12 {
|
||||
t.Errorf("NonceSize = %d, want 12", a.NonceSize())
|
||||
}
|
||||
if a.KeySize() != 32 {
|
||||
t.Errorf("KeySize = %d, want 32", a.KeySize())
|
||||
}
|
||||
if a.Overhead() != 16 {
|
||||
t.Errorf("Overhead = %d, want 16", a.Overhead())
|
||||
}
|
||||
|
||||
sealed := a.Seal(nil, nonce, pt, aad)
|
||||
// stdlib produces ciphertext || tag in one buffer.
|
||||
if len(sealed) != len(pt)+16 {
|
||||
t.Fatalf("Seal len = %d, want %d", len(sealed), len(pt)+16)
|
||||
}
|
||||
gotCT := sealed[:len(pt)]
|
||||
gotTag := sealed[len(pt):]
|
||||
if !bytes.Equal(gotCT, wantCT) {
|
||||
t.Errorf("ciphertext mismatch\n got %s\n want %s",
|
||||
hex.EncodeToString(gotCT),
|
||||
hex.EncodeToString(wantCT))
|
||||
}
|
||||
if !bytes.Equal(gotTag, wantTag) {
|
||||
t.Errorf("tag mismatch\n got %s\n want %s",
|
||||
hex.EncodeToString(gotTag),
|
||||
hex.EncodeToString(wantTag))
|
||||
}
|
||||
|
||||
// Round-trip Open.
|
||||
pt2, err := a.Open(nil, nonce, sealed, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
if !bytes.Equal(pt2, pt) {
|
||||
t.Errorf("Open plaintext mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTamperedCiphertext -- flipping any bit in the ciphertext must cause
|
||||
// Open to fail (constant-time tag verify).
|
||||
func TestTamperedCiphertext(t *testing.T) {
|
||||
v := vectors[0]
|
||||
key := unhex(t, v.key)
|
||||
nonce := unhex(t, v.nonce)
|
||||
aad := unhex(t, v.aad)
|
||||
pt := unhex(t, v.plaintext)
|
||||
|
||||
a, err := NewChaCha20Poly1305(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewChaCha20Poly1305: %v", err)
|
||||
}
|
||||
sealed := a.Seal(nil, nonce, pt, aad)
|
||||
|
||||
tampered := make([]byte, len(sealed))
|
||||
copy(tampered, sealed)
|
||||
tampered[5] ^= 0x01
|
||||
|
||||
if _, err := a.Open(nil, nonce, tampered, aad); err == nil {
|
||||
t.Error("Open should have rejected tampered ciphertext")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTamperedAAD -- flipping any bit in AAD must cause Open to fail.
|
||||
func TestTamperedAAD(t *testing.T) {
|
||||
v := vectors[0]
|
||||
key := unhex(t, v.key)
|
||||
nonce := unhex(t, v.nonce)
|
||||
aad := unhex(t, v.aad)
|
||||
pt := unhex(t, v.plaintext)
|
||||
|
||||
a, err := NewChaCha20Poly1305(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewChaCha20Poly1305: %v", err)
|
||||
}
|
||||
sealed := a.Seal(nil, nonce, pt, aad)
|
||||
|
||||
badAAD := make([]byte, len(aad))
|
||||
copy(badAAD, aad)
|
||||
badAAD[3] ^= 0x80
|
||||
|
||||
if _, err := a.Open(nil, nonce, sealed, badAAD); err == nil {
|
||||
t.Error("Open should have rejected tampered AAD")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTamperedTag -- flipping any bit in the tag must cause Open to fail.
|
||||
func TestTamperedTag(t *testing.T) {
|
||||
v := vectors[0]
|
||||
key := unhex(t, v.key)
|
||||
nonce := unhex(t, v.nonce)
|
||||
aad := unhex(t, v.aad)
|
||||
pt := unhex(t, v.plaintext)
|
||||
|
||||
a, err := NewChaCha20Poly1305(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewChaCha20Poly1305: %v", err)
|
||||
}
|
||||
sealed := a.Seal(nil, nonce, pt, aad)
|
||||
|
||||
tampered := make([]byte, len(sealed))
|
||||
copy(tampered, sealed)
|
||||
// Flip a bit in the tag (last 16 bytes).
|
||||
tampered[len(tampered)-1] ^= 0x40
|
||||
|
||||
if _, err := a.Open(nil, nonce, sealed, aad); err != nil {
|
||||
t.Fatalf("Open with valid tag failed: %v", err)
|
||||
}
|
||||
if _, err := a.Open(nil, nonce, tampered, aad); err == nil {
|
||||
t.Error("Open should have rejected tampered tag")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyInputs -- AEAD must accept zero-length plaintext and zero-length AAD.
|
||||
func TestEmptyInputs(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x42}, 32)
|
||||
nonce := bytes.Repeat([]byte{0x07}, 12)
|
||||
|
||||
a, err := NewChaCha20Poly1305(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewChaCha20Poly1305: %v", err)
|
||||
}
|
||||
sealed := a.Seal(nil, nonce, nil, nil)
|
||||
if len(sealed) != 16 {
|
||||
t.Errorf("Seal empty len = %d, want 16 (tag only)", len(sealed))
|
||||
}
|
||||
pt, err := a.Open(nil, nonce, sealed, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Open empty: %v", err)
|
||||
}
|
||||
if len(pt) != 0 {
|
||||
t.Errorf("Open plaintext len = %d, want 0", len(pt))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAES256GCM -- the second AEAD in this package; verify its surface.
|
||||
func TestAES256GCM(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0x77}, 32)
|
||||
nonce := bytes.Repeat([]byte{0x42}, 12)
|
||||
pt := []byte("hello, AEAD")
|
||||
aad := []byte("auth-only-data")
|
||||
|
||||
a, err := NewAES256GCM(key)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAES256GCM: %v", err)
|
||||
}
|
||||
if a.NonceSize() != 12 {
|
||||
t.Errorf("NonceSize = %d, want 12", a.NonceSize())
|
||||
}
|
||||
if a.Overhead() != 16 {
|
||||
t.Errorf("Overhead = %d, want 16", a.Overhead())
|
||||
}
|
||||
if a.KeySize() != 32 {
|
||||
t.Errorf("KeySize = %d, want 32", a.KeySize())
|
||||
}
|
||||
|
||||
sealed := a.Seal(nil, nonce, pt, aad)
|
||||
if len(sealed) != len(pt)+16 {
|
||||
t.Errorf("Seal len = %d, want %d", len(sealed), len(pt)+16)
|
||||
}
|
||||
pt2, err := a.Open(nil, nonce, sealed, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
if !bytes.Equal(pt2, pt) {
|
||||
t.Errorf("plaintext mismatch")
|
||||
}
|
||||
|
||||
// Tamper -> Open fails.
|
||||
sealed[0] ^= 0x01
|
||||
if _, err := a.Open(nil, nonce, sealed, aad); err == nil {
|
||||
t.Error("Open should have rejected tampered ciphertext")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAEAD -- factory dispatches correctly.
|
||||
func TestGetAEAD(t *testing.T) {
|
||||
key := bytes.Repeat([]byte{0xab}, 32)
|
||||
|
||||
chacha, err := GetAEAD(ChaCha20Poly1305, key)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAEAD ChaCha20Poly1305: %v", err)
|
||||
}
|
||||
if chacha == nil {
|
||||
t.Fatal("GetAEAD returned nil")
|
||||
}
|
||||
|
||||
gcm, err := GetAEAD(AES256GCM, key)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAEAD AES256GCM: %v", err)
|
||||
}
|
||||
if gcm == nil {
|
||||
t.Fatal("GetAEAD returned nil")
|
||||
}
|
||||
|
||||
if _, err := GetAEAD("nonexistent", key); err == nil {
|
||||
t.Error("GetAEAD with unknown ID should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonceGenerator -- deterministic stream-based nonce derivation.
|
||||
func TestNonceGenerator(t *testing.T) {
|
||||
ng := NewNonceGenerator(0x01020304)
|
||||
first := ng.Next()
|
||||
second := ng.Next()
|
||||
|
||||
if len(first) != 12 {
|
||||
t.Errorf("nonce len = %d, want 12", len(first))
|
||||
}
|
||||
if bytes.Equal(first, second) {
|
||||
t.Error("two consecutive nonces are equal -- catastrophic for AEAD")
|
||||
}
|
||||
// First 4 bytes = stream ID big-endian.
|
||||
want := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
if !bytes.Equal(first[:4], want) {
|
||||
t.Errorf("stream-ID prefix = %x, want %x", first[:4], want)
|
||||
}
|
||||
// First nonce should have seqNo = 0.
|
||||
zeros := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
if !bytes.Equal(first[4:], zeros) {
|
||||
t.Errorf("first seqNo = %x, want zeros", first[4:])
|
||||
}
|
||||
if ng.GetSeqNo() != 2 {
|
||||
t.Errorf("seqNo after two Next() = %d, want 2", ng.GetSeqNo())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user