Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ add_subdirectory(atf)
add_subdirectory(fuzz)
add_subdirectory(toolchain)
add_subdirectory(x509)
add_subdirectory(ccm)
add_subdirectory(tls13_handshake_fault)
add_subdirectory(leak)
add_subdirectory(concurrent)
Expand Down
22 changes: 22 additions & 0 deletions tests/ccm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# N-147/N-148: the vendored AES-CCM against independent known answers.
#
# HOST ONLY, as tests/x509. Links tests/x509's nx_crypto archive, which is
# the whole of crypto_libraries/src under the shipping definitions, so this
# directory has to be added after x509. The method objects in
# nx_crypto_methods.c are what TLS calls, and they reach every cipher, which
# is why the archive and not a source list.
#
# python3 tests/ccm/gen_ccm_vectors.py > tests/ccm/ccm_vectors.h
#
# SPDX-License-Identifier: MIT

if(CMAKE_CROSSCOMPILING)
return()
endif()

add_executable(test_ccm_contract test_ccm_contract.c)
target_include_directories(test_ccm_contract PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(test_ccm_contract PRIVATE test_x509_nx_crypto)
target_compile_options(test_ccm_contract PRIVATE -Wall -Wextra -UAMINETXDUO_IPV6)

add_test(NAME ccm_contract COMMAND test_ccm_contract)
2,187 changes: 2,187 additions & 0 deletions tests/ccm/ccm_vectors.h

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions tests/ccm/gen_ccm_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
# AES-128-CCM known answers for test_ccm_contract.c, from an independent
# implementation: Python `cryptography` AESCCM (OpenSSL underneath).
#
# python3 tests/ccm/gen_ccm_vectors.py > tests/ccm/ccm_vectors.h
#
# The plaintext and AAD are the patterns the test regenerates. CTR output
# does not depend on the AAD or the tag length, and every plaintext is a
# prefix of the longest, so one ciphertext per nonce covers every case for
# that nonce; the generator asserts that rather than assuming it.
#
# SPDX-License-Identifier: MIT
import sys
import cryptography
from cryptography.hazmat.primitives.ciphers.aead import AESCCM
from cryptography.hazmat.backends.openssl.backend import backend

KEY = bytes(range(0x40, 0x50))
NONCE_BYTES = bytes(range(0x10, 0x1d)) # a nonce of n bytes is the first n
TAGS = [16, 8]

# (nonce length, payloads, AADs). 12 bytes is TLS (L = 3); 7 and 13 are the
# RFC 3610 ends, L = 8 and L = 2.
GROUPS = [
(12, [0, 1, 16, 4080, 4096, 4112, 16384], [0, 5, 13]),
(12, [0, 16, 4096], [65279, 65280, 70000]), # last 2-octet l(a), first 6-octet, beyond
(7, [0, 16, 4080, 4096, 4112], [0, 13]),
(13, [0, 16, 4080, 4096, 4112], [0, 13]),
]

def pt(n): return bytes(((i * 31) + 7) & 0xFF for i in range(n))
def aad(n): return bytes(((i * 13) + 101) & 0xFF for i in range(n))

maxlen = {}
for nl, payloads, _ in GROUPS:
maxlen[nl] = max(maxlen.get(nl, 0), max(payloads))

full = {}
for nl, m in maxlen.items():
full[nl] = AESCCM(KEY, tag_length=16).encrypt(NONCE_BYTES[:nl], pt(m), None)[:m]

cases = []
for nl, payloads, aads in GROUPS:
nonce = NONCE_BYTES[:nl]
for p in payloads:
for a in aads:
for t in TAGS:
ad = aad(a) if a else None
out = AESCCM(KEY, tag_length=t).encrypt(nonce, pt(p), ad)
assert out[:p] == full[nl][:p]
assert AESCCM(KEY, tag_length=t).decrypt(nonce, out, ad) == pt(p)
cases.append((nl, p, a, t, out[p:]))

def arr(b, ind=" "):
return "\n".join(ind + ", ".join("0x%02x" % x for x in b[i:i + 12]) + ","
for i in range(0, len(b), 12))

w = sys.stdout.write
w("/* Generated by tests/ccm/gen_ccm_vectors.py: Python %s, cryptography %s, %s.\n"
" Do not edit. */\n\n" % (sys.version.split()[0], cryptography.__version__,
backend.openssl_version_text()))
w("#define CCM_VEC_MAXLEN %d\n\n" % max(maxlen.values()))
w("static const unsigned char ccm_vec_key[16] = {\n%s\n};\n\n" % arr(KEY))
w("static const unsigned char ccm_vec_nonce[13] = {\n%s\n};\n\n" % arr(NONCE_BYTES))
for nl in sorted(maxlen):
w("static const unsigned char ccm_vec_ct%d[%d] = {\n%s\n};\n\n" % (nl, maxlen[nl], arr(full[nl])))
w("static const struct { unsigned nonce_len, payload, aad, tag_len; unsigned char tag[16]; }\n"
"ccm_vec_cases[] = {\n")
for nl, p, a, t, tag in cases:
w(" { %2u, %5u, %5u, %2u, { %s } },\n" % (nl, p, a, t, ", ".join("0x%02x" % x for x in tag)))
w("};\n\n")
w("static const unsigned char *ccm_vec_ciphertext(unsigned nonce_len)\n{\n")
for nl in sorted(maxlen):
w(" if (nonce_len == %d) return ccm_vec_ct%d;\n" % (nl, nl))
w(" return 0;\n}\n")
223 changes: 223 additions & 0 deletions tests/ccm/test_ccm_contract.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* N-147/N-148: the vendored AES-CCM against independent known answers.
*
* Every vector is Python `cryptography`'s AESCCM (gen_ccm_vectors.py), so a
* NetX build that only agrees with itself cannot pass. Payloads straddle the
* counter: 4080 bytes is 255 blocks, 4096 the 256th (the first carry out of
* the low counter octet), 16384 the TLS record cap. AAD 5 and 13 are the TLS
* 1.3 and 1.2 headers; 0, 65279, 65280 and 70000 are RFC 3610 2.2's empty,
* 2-octet and 6-octet l(a) forms. Tags 16 and 8 are the two CCM methods.
* Nonces are 12 bytes (TLS, L = 3) plus the RFC 3610 ends, 7 (L = 8) and
* 13 (L = 2).
*
* Each case runs the record layer's three-step path (INITIALIZE, one UPDATE
* for the whole record, CALCULATE) both ways, the one-shot ENCRYPT/DECRYPT,
* and a decrypt with one tag bit flipped, which must be refused.
*
* The whole-record UPDATE is one shape only: it is what our record layer
* does today, but a caller may split a message across UPDATEs. The split
* control below covers the one split that matters to the counter, block
* aligned at 255 blocks so the second UPDATE starts on counter 256. The
* mode itself is not a streaming API for splits that are not block aligned
* (each UPDATE pads its own MAC input and restarts its keystream block), and
* nothing here claims otherwise.
*
* Output is key=value; the exit status is the verdict.
*
* SPDX-License-Identifier: MIT
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "nx_crypto_aes.h"
#include "ccm_vectors.h"

extern NX_CRYPTO_METHOD crypto_method_aes_ccm_8;
extern NX_CRYPTO_METHOD crypto_method_aes_ccm_16;

#define CCM_VEC_MAXAAD 70000
#define CCM_SPLIT (255 * 16)

static NX_CRYPTO_AES ctx __attribute__((aligned(8)));
static UCHAR plain[CCM_VEC_MAXLEN];
static UCHAR aad[CCM_VEC_MAXAAD];
static UCHAR out[CCM_VEC_MAXLEN + 16];
static UCHAR back[CCM_VEC_MAXLEN + 16];
static UCHAR iv[14];

static int failures;
static int checks;

static NX_CRYPTO_METHOD *method_for(unsigned tag_len)
{
return (tag_len == 8) ? &crypto_method_aes_ccm_8 : &crypto_method_aes_ccm_16;
}

static UINT keyed(NX_CRYPTO_METHOD *m)
{
VOID *handle = NX_CRYPTO_NULL;

memset(&ctx, 0, sizeof(ctx));
return m -> nx_crypto_init(m, (UCHAR *)ccm_vec_key, 128, &handle, &ctx, sizeof(ctx));
}

static UINT op(NX_CRYPTO_METHOD *m, UINT o, UCHAR *in, ULONG in_len, UCHAR *o_ptr, ULONG o_len)
{
return m -> nx_crypto_operation(o, NX_CRYPTO_NULL, m, (UCHAR *)ccm_vec_key, 128,
in, in_len, iv, o_ptr, o_len, &ctx, sizeof(ctx),
NX_CRYPTO_NULL, NX_CRYPTO_NULL);
}

/* First 16-byte block where got differs from the reference ciphertext, or -1. */
static long first_bad_block(const UCHAR *got, const UCHAR *ref, unsigned len)
{
unsigned i;

for (i = 0; i < len; i++)
{
if (got[i] != ref[i])
{
return (long)(i / 16);
}
}
return -1;
}

static void expect(int ok, unsigned idx, const char *path, const char *what, long block)
{
checks++;
if (!ok)
{
failures++;
printf("fail case=%u nonce=%u payload=%u aad=%u tag=%u path=%s what=%s block=%ld\n",
idx, ccm_vec_cases[idx].nonce_len, ccm_vec_cases[idx].payload,
ccm_vec_cases[idx].aad, ccm_vec_cases[idx].tag_len, path, what, block);
}
}

/* Record path, both ways, with the payload given to UPDATE in pieces of at
most `split` bytes (block aligned). split == p is the whole record. */
static void record_path(unsigned idx, unsigned split, const char *path)
{
unsigned p = ccm_vec_cases[idx].payload;
unsigned a = ccm_vec_cases[idx].aad;
unsigned t = ccm_vec_cases[idx].tag_len;
const UCHAR *tag = ccm_vec_cases[idx].tag;
const UCHAR *ct = ccm_vec_ciphertext(ccm_vec_cases[idx].nonce_len);
NX_CRYPTO_METHOD *m = method_for(t);
UCHAR *a_ptr = a ? aad : NX_CRYPTO_NULL;
UCHAR icv[16];
UINT st;
unsigned off, n;

/* Encrypt: as nx_secure_tls_record_payload_encrypt.c when split == p. */
memset(out, 0xA5, sizeof(out));
st = keyed(m);
st |= op(m, NX_CRYPTO_ENCRYPT_INITIALIZE, a_ptr, a, NX_CRYPTO_NULL, p);
off = 0;
do
{
n = ((p - off) > split) ? split : (p - off);
st |= op(m, NX_CRYPTO_ENCRYPT_UPDATE, plain + off, n, out + off, n);
off += n;
} while (off < p);
st |= op(m, NX_CRYPTO_ENCRYPT_CALCULATE, NX_CRYPTO_NULL, 0, icv, t);
expect(st == NX_CRYPTO_SUCCESS, idx, path, "encrypt_status", -1);
expect(memcmp(out, ct, p) == 0, idx, path, "ciphertext", first_bad_block(out, ct, p));
expect(memcmp(icv, tag, t) == 0, idx, path, "tag", -1);

/* Decrypt. */
memset(back, 0xA5, sizeof(back));
memcpy(icv, tag, t);
st = keyed(m);
st |= op(m, NX_CRYPTO_DECRYPT_INITIALIZE, a_ptr, a, NX_CRYPTO_NULL, p);
off = 0;
do
{
n = ((p - off) > split) ? split : (p - off);
st |= op(m, NX_CRYPTO_DECRYPT_UPDATE, (UCHAR *)ct + off, n, back + off, n);
off += n;
} while (off < p);
st |= op(m, NX_CRYPTO_DECRYPT_CALCULATE, icv, t, NX_CRYPTO_NULL, 0);
expect(st == NX_CRYPTO_SUCCESS, idx, path, "decrypt_status", -1);
expect(memcmp(back, plain, p) == 0, idx, path, "plaintext", -1);
}

static void run_case(unsigned idx)
{
unsigned p = ccm_vec_cases[idx].payload;
unsigned a = ccm_vec_cases[idx].aad;
unsigned t = ccm_vec_cases[idx].tag_len;
const UCHAR *tag = ccm_vec_cases[idx].tag;
const UCHAR *ct = ccm_vec_ciphertext(ccm_vec_cases[idx].nonce_len);
NX_CRYPTO_METHOD *m = method_for(t);
UCHAR *a_ptr = a ? aad : NX_CRYPTO_NULL;
UINT st;

iv[0] = (UCHAR)ccm_vec_cases[idx].nonce_len;
memcpy(iv + 1, ccm_vec_nonce, ccm_vec_cases[idx].nonce_len);

record_path(idx, p, "record");

/* Split control: 255 blocks, then the rest from counter 256. */
if (p > CCM_SPLIT)
{
record_path(idx, CCM_SPLIT, "split255");
}

/* One shot, encrypt: tag appended to the ciphertext. */
memset(out, 0xA5, sizeof(out));
st = keyed(m);
st |= op(m, NX_CRYPTO_SET_ADDITIONAL_DATA, a_ptr, a, NX_CRYPTO_NULL, 0);
st |= op(m, NX_CRYPTO_ENCRYPT, plain, p, out, p + t);
expect(st == NX_CRYPTO_SUCCESS, idx, "oneshot", "encrypt_status", -1);
expect(memcmp(out, ct, p) == 0, idx, "oneshot", "ciphertext", first_bad_block(out, ct, p));
expect(memcmp(out + p, tag, t) == 0, idx, "oneshot", "tag", -1);

/* One shot, decrypt the reference. */
memcpy(out, ct, p);
memcpy(out + p, tag, t);
memset(back, 0xA5, sizeof(back));
st = keyed(m);
st |= op(m, NX_CRYPTO_SET_ADDITIONAL_DATA, a_ptr, a, NX_CRYPTO_NULL, 0);
st |= op(m, NX_CRYPTO_DECRYPT, out, p + t, back, p);
expect(st == NX_CRYPTO_SUCCESS, idx, "oneshot", "decrypt_status", -1);
expect(memcmp(back, plain, p) == 0, idx, "oneshot", "plaintext", -1);

/* A flipped tag bit is refused. */
out[p + t - 1] ^= 0x01;
st = keyed(m);
st |= op(m, NX_CRYPTO_SET_ADDITIONAL_DATA, a_ptr, a, NX_CRYPTO_NULL, 0);
st = op(m, NX_CRYPTO_DECRYPT, out, p + t, back, p);
expect(st == NX_CRYPTO_AUTHENTICATION_FAILED, idx, "oneshot", "tamper_refused", -1);
}

int main(void)
{
unsigned i;
unsigned n = (unsigned)(sizeof(ccm_vec_cases) / sizeof(ccm_vec_cases[0]));
int before;

for (i = 0; i < CCM_VEC_MAXLEN; i++)
{
plain[i] = (UCHAR)((i * 31) + 7);
}
for (i = 0; i < CCM_VEC_MAXAAD; i++)
{
aad[i] = (UCHAR)((i * 13) + 101);
}

for (i = 0; i < n; i++)
{
before = failures;
run_case(i);
printf("case=%u nonce=%u payload=%u aad=%u tag=%u result=%s\n", i,
ccm_vec_cases[i].nonce_len, ccm_vec_cases[i].payload, ccm_vec_cases[i].aad,
ccm_vec_cases[i].tag_len, (failures == before) ? "pass" : "FAIL");
}

printf("ccm_contract cases=%u checks=%d failures=%d\n", n, checks, failures);
return failures ? 1 : 0;
}
2 changes: 1 addition & 1 deletion third_party/netxduo
6 changes: 4 additions & 2 deletions tools/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@ host_test_targets() { # builddir
# the optional server client-certificate feature, as in shipped images.
# 547 with tls_x509_field_bounds (N-128/N-129/N-130/E-234): the X.509
# certificate parser on re-encoded certificates (all hosts).
HOST_TESTS_EXPECTED=547
# 548 with ccm_contract (N-147/N-148): AES-CCM against independent
# known answers across the 255/256 counter carry (all hosts).
HOST_TESTS_EXPECTED=548
case "$(uname -m)" in
x86_64|amd64) ;;
# test_inet, test_route, test_expunge, test_expunge_cork, test_select,
Expand All @@ -481,7 +483,7 @@ case "$(uname -m)" in
*) HOST_TESTS_EXPECTED=$((HOST_TESTS_EXPECTED - 30)) ;;
esac
# The conflict-drain fixture uses Linux's MAP_32BIT, not merely x86_64.
# Darwin arm64 registers 513; Darwin x86_64 registers 543 on this tree.
# Darwin arm64 registers 517; Darwin x86_64 registers 547 on this tree.
case "$(uname -s):$(uname -m)" in
Linux:x86_64|Linux:amd64) ;;
*) HOST_TESTS_EXPECTED=$((HOST_TESTS_EXPECTED - 1)) ;;
Expand Down
Loading