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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ version number before tagging the release.
`malloc(sizeof(T))`, which is layout-exact and immune. `@c_struct` overlays
(C-defined size) and raw uncast buffers are not flagged.

- **Wycheproof wave 6: ECDSA secp256k1, Ed448, and AES-CMAC.** More adversarial
vector coverage over implemented primitives that had none:
- **ECDSA secp256k1** (the Bitcoin/Ethereum curve), DER (via the real
`tls13_cert.split_ecdsa_sig` parser) and P1363 raw-r||s forms.
- **Ed448** signature verify — the decode/verify surface (non-canonical point
encodings, small-order points, s-range, wrong lengths) that the ed25519
driver's tc151 forgery lived in, now covered for the larger curve.
- **AES-CMAC** (RFC 4493) tag verification, including forged/truncated tags
and illegal key lengths.

All pass with no accepted forgeries. AES-CMAC sweeps all 311 cases by default
(symmetric-fast); the ECDSA/Ed448 drivers stride-sample under the 180s harness
budget with `WYCHEPROOF_FULL=1` sweeping everything. Vectors vendored from
C2SP/wycheproof.

### Changed

- **`std` now allocates its context structs with `malloc(sizeof(T))`.** Swept 65
Expand Down
27 changes: 27 additions & 0 deletions tests/integration/wycheproof/test_wycheproof_ecdsa_secp256k1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
# Wycheproof adversarial vector suites — ECDSA secp256k1 (P1363 + DER), wave 6.
#
# The Bitcoin/Ethereum curve. Its own harness slot: a secp256k1 verify is two
# 256-bit bignum scalar multiplications (~1-2s each at CI's -O0). Default stride
# 10; WYCHEPROOF_FULL=1 (the nightly) sweeps all, WYCHEPROOF_STRIDE=N overrides.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
cd "$ROOT" || exit 1

AE="$ROOT/build/ae"
[ -n "${EXE_EXT:-}" ] && AE="$AE$EXE_EXT"
[ -x "$AE" ] || { echo " [FAIL] wycheproof_ecdsa_secp256k1: build/ae missing (run make)"; exit 1; }

rc=0
for drv in wp_ecdsa_secp256k1 wp_ecdsa_secp256k1_der; do
out="$("$AE" run "tests/integration/wycheproof/$drv.ae" 2>&1)"
if printf '%s' "$out" | grep -q "^ALL PASS"; then
printf '%s\n' "$out" | grep "^wycheproof" | sed 's/^/ [PASS] /'
else
echo " [FAIL] wycheproof $drv:"
printf '%s\n' "$out" | tail -12 | sed 's/^/ /'
rc=1
fi
done
exit $rc
28 changes: 28 additions & 0 deletions tests/integration/wycheproof/test_wycheproof_ed448_cmac.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/sh
# Wycheproof adversarial vector suites — Ed448 signature verify + AES-CMAC, wave 6.
#
# Two families sharing one harness slot. AES-CMAC is symmetric-fast (full sweep
# by default). Ed448 verify is bignum-heavy (~4s each), so it stride-samples
# (default 4); WYCHEPROOF_FULL=1 (the nightly) sweeps all 87, WYCHEPROOF_STRIDE=N
# overrides both.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
cd "$ROOT" || exit 1

AE="$ROOT/build/ae"
[ -n "${EXE_EXT:-}" ] && AE="$AE$EXE_EXT"
[ -x "$AE" ] || { echo " [FAIL] wycheproof_ed448_cmac: build/ae missing (run make)"; exit 1; }

rc=0
for drv in wp_aes_cmac wp_ed448; do
out="$("$AE" run "tests/integration/wycheproof/$drv.ae" 2>&1)"
if printf '%s' "$out" | grep -q "^ALL PASS"; then
printf '%s\n' "$out" | grep "^wycheproof" | sed 's/^/ [PASS] /'
else
echo " [FAIL] wycheproof $drv:"
printf '%s\n' "$out" | tail -12 | sed 's/^/ /'
rc=1
fi
done
exit $rc
145 changes: 145 additions & 0 deletions tests/integration/wycheproof/wp_aes_cmac.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Wycheproof adversarial vectors — AES-CMAC (RFC 4493) verification (wave 6).
//
// CMAC is a MAC, so "verify" is: recompute the tag over (key, msg), truncate
// to the group's tagSize, and compare to the expected tag in constant intent.
// "valid" -> recomputed tag MUST equal the expected tag.
// "invalid" -> it MUST NOT (a forged/tampered tag, or an illegal key length,
// must be rejected).
// AES keys are 128/192/256-bit; groups with any other keySize (0/8/64/160/320)
// are illegal and always "invalid" — the driver rejects them without invoking
// cmac on an unsupported key length.
// Fast (symmetric); sweeps ALL cases by default. WYCHEPROOF_STRIDE=N can thin.

import std.cryptography.aes
import std.os
import std.fs
import std.json
import std.bytes
import std.string
import std.io

extern exit(code: int)
extern atoi(s: string) -> int
extern string_char_at_n(str: string, known_length: int, index: int) -> int
extern string_length(s: string) -> int

fn hexval(h: int) -> int {
if h >= 48 && h <= 57 { return h - 48 }
if h >= 97 && h <= 102 { return h - 87 }
if h >= 65 && h <= 70 { return h - 55 }
return 0
}

fn from_hex(s: string) -> ptr {
slen = string_length(s)
n = slen / 2
b = bytes.new(n)
if n == 0 { return b }
i = 0
while i < n {
hi = hexval(string_char_at_n(s, slen, i * 2))
lo = hexval(string_char_at_n(s, slen, i * 2 + 1))
bytes.set(b, i, (hi << 4) | lo)
i = i + 1
}
return b
}

// Compare the first `tlen` bytes of the recomputed 16-byte CMAC against the
// expected tag buffer (also `tlen` bytes).
fn tag_eq(got: ptr, want: ptr, tlen: int) -> int {
i = 0
while i < tlen {
if bytes.get(got, i) != bytes.get(want, i) { return 0 }
i = i + 1
}
return 1
}

main() {
stride = 1
sv = os.os_getenv("WYCHEPROOF_STRIDE")
if sv != 0 { stride = atoi(sv) }
if stride < 1 { stride = 1 }

doc, ferr = fs.read("tests/vectors/wycheproof/aes_cmac_test.json")
if ferr != "" {
println("FAIL: cannot read vector file: ${ferr}")
exit(1)
}
root, jerr = json.parse(doc)
if jerr != "" {
println("FAIL: vector JSON parse: ${jerr}")
exit(1)
}

groups = json.json_object_get_raw(root, "testGroups")
ngroups = json.array_size(groups)

passed = 0
rejected_invalid = 0
failed = 0
gi = 0
while gi < ngroups {
g = json.array_get_raw(groups, gi)
tag_bits = json.json_get_int(json.json_object_get_raw(g, "tagSize"))
tlen = tag_bits / 8
tests = json.json_object_get_raw(g, "tests")
nt = json.array_size(tests)
ti = 0
while ti < nt {
t = json.array_get_raw(tests, ti)
tc_id = json.json_get_int(json.json_object_get_raw(t, "tcId"))
if (tc_id % stride) != 0 && stride > 1 {
ti = ti + 1
continue
}
res = json.json_get_string_raw(json.json_object_get_raw(t, "result"))
key_hex = json.json_get_string_raw(json.json_object_get_raw(t, "key"))
msg_hex = json.json_get_string_raw(json.json_object_get_raw(t, "msg"))
tag_hex = json.json_get_string_raw(json.json_object_get_raw(t, "tag"))

keylen = string_length(key_hex) / 2

// Only 128/192/256-bit keys are legal AES-CMAC keys. Anything else
// is an illegal-length case Wycheproof marks "invalid" — reject
// without computing (aes.cmac has no defined behaviour there).
ok = 0
if keylen == 16 || keylen == 24 || keylen == 32 {
key = from_hex(key_hex)
msg = from_hex(msg_hex)
want = from_hex(tag_hex)
got = aes.cmac(key, keylen, msg, string_length(msg_hex) / 2)
if tlen <= 16 && string_length(tag_hex) / 2 == tlen {
ok = tag_eq(got, want, tlen)
}
bytes.free(key)
bytes.free(msg)
bytes.free(want)
bytes.free(got)
}

if res == "valid" {
if ok == 1 { passed = passed + 1 } else {
println("FAIL tcId=${tc_id} (valid): recomputed tag != expected")
failed = failed + 1
}
} else {
if res == "invalid" {
if ok == 0 { rejected_invalid = rejected_invalid + 1 } else {
println("FAIL tcId=${tc_id} (invalid): tag ACCEPTED")
failed = failed + 1
}
} else {
passed = passed + 1
}
}
ti = ti + 1
}
gi = gi + 1
}

println("wycheproof aes_cmac: ${passed} ok, ${rejected_invalid} invalid rejected, ${failed} failed (stride ${stride})")
if failed > 0 { exit(1) }
println("ALL PASS")
}
172 changes: 172 additions & 0 deletions tests/integration/wycheproof/wp_ecdsa_secp256k1.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Wycheproof adversarial vectors — ECDSA secp256k1 / SHA-256, P1363 (wave 6).
//
// Raw r||s (64 bytes). Measures secp256k1.ecdsa_verify directly (r/s range +
// modular edge cases); the DER parser is the wp_ecdsa_secp256k1_der.ae sibling.
// "valid" -> verify MUST return 1.
// "invalid" -> verify MUST return 0.
// Stride sampling (default 10; WYCHEPROOF_FULL=1 or WYCHEPROOF_STRIDE=N).

import std.cryptography.secp256k1
import std.cryptography.sha2
import std.os
import std.fs
import std.json
import std.bytes
import std.string
import std.io

extern exit(code: int)
extern atoi(s: string) -> int
extern string_char_at_n(str: string, known_length: int, index: int) -> int
extern string_length(s: string) -> int

fn hexval(h: int) -> int {
if h >= 48 && h <= 57 { return h - 48 }
if h >= 97 && h <= 102 { return h - 87 }
if h >= 65 && h <= 70 { return h - 55 }
return 0
}

fn from_hex(s: string) -> ptr {
slen = string_length(s)
n = slen / 2
b = bytes.new(n)
if n == 0 { return b }
i = 0
while i < n {
hi = hexval(string_char_at_n(s, slen, i * 2))
lo = hexval(string_char_at_n(s, slen, i * 2 + 1))
bytes.set(b, i, (hi << 4) | lo)
i = i + 1
}
return b
}

fn coordn(hexs: string, width: int) -> ptr {
raw = from_hex(hexs)
n = string_length(hexs) / 2
out = bytes.new(width)
i = 0
while i < width { bytes.set(out, i, 0) i = i + 1 }
if n >= width {
i = 0
while i < width { bytes.set(out, i, bytes.get(raw, n - width + i)) i = i + 1 }
} else {
i = 0
while i < n { bytes.set(out, width - n + i, bytes.get(raw, i)) i = i + 1 }
}
bytes.free(raw)
return out
}

fn slicen(sig: ptr, off: int, width: int) -> ptr {
out = bytes.new(width)
i = 0
while i < width { bytes.set(out, i, bytes.get(sig, off + i)) i = i + 1 }
return out
}

fn sha256_of(b: ptr, n: int) -> ptr {
ctx, e = sha2.new("sha256")
sha2.update_bytes(ctx, b, n)
return sha2.final_bytes(ctx)
}

main() {
stride = 10
wf = os.os_getenv("WYCHEPROOF_FULL")
if wf != 0 {
if wf == "1" { stride = 1 }
}
sv = os.os_getenv("WYCHEPROOF_STRIDE")
if sv != 0 { stride = atoi(sv) }
if stride < 1 { stride = 1 }

doc, ferr = fs.read("tests/vectors/wycheproof/ecdsa_secp256k1_sha256_p1363_test.json")
if ferr != "" {
println("FAIL: cannot read vector file: ${ferr}")
exit(1)
}
root, jerr = json.parse(doc)
if jerr != "" {
println("FAIL: vector JSON parse: ${jerr}")
exit(1)
}

groups = json.json_object_get_raw(root, "testGroups")
ngroups = json.array_size(groups)

passed = 0
rejected_invalid = 0
failed = 0
gi = 0
while gi < ngroups {
g = json.array_get_raw(groups, gi)
pkobj = json.json_object_get_raw(g, "publicKey")
wx = json.json_get_string_raw(json.json_object_get_raw(pkobj, "wx"))
wy = json.json_get_string_raw(json.json_object_get_raw(pkobj, "wy"))
pubx = coordn(wx, 32)
puby = coordn(wy, 32)
tests = json.json_object_get_raw(g, "tests")
nt = json.array_size(tests)
ti = 0
while ti < nt {
t = json.array_get_raw(tests, ti)
tc_id = json.json_get_int(json.json_object_get_raw(t, "tcId"))
if (tc_id % stride) != 0 && stride > 1 {
ti = ti + 1
continue
}
res = json.json_get_string_raw(json.json_object_get_raw(t, "result"))
msg_hex = json.json_get_string_raw(json.json_object_get_raw(t, "msg"))
sig_hex = json.json_get_string_raw(json.json_object_get_raw(t, "sig"))

// A well-formed secp256k1 P1363 sig is exactly 64 bytes (128 hex).
if string_length(sig_hex) != 128 {
if res == "valid" {
println("FAIL tcId=${tc_id}: valid case with non-64-byte sig?")
failed = failed + 1
} else {
rejected_invalid = rejected_invalid + 1
}
ti = ti + 1
continue
}

msg = from_hex(msg_hex)
h = sha256_of(msg, string_length(msg_hex) / 2)
sig = from_hex(sig_hex)
r = slicen(sig, 0, 32)
s = slicen(sig, 32, 32)
ok = secp256k1.ecdsa_verify(pubx, puby, h, r, s)
if res == "valid" {
if ok == 1 { passed = passed + 1 } else {
println("FAIL tcId=${tc_id} (valid): verify rejected a good signature")
failed = failed + 1
}
} else {
if res == "invalid" {
if ok == 0 { rejected_invalid = rejected_invalid + 1 } else {
println("FAIL tcId=${tc_id} (invalid): verify ACCEPTED a forgery")
failed = failed + 1
}
} else {
passed = passed + 1
}
}
bytes.free(msg)
bytes.free(h)
bytes.free(sig)
bytes.free(r)
bytes.free(s)
ti = ti + 1
}
bytes.free(pubx)
bytes.free(puby)
gi = gi + 1
}

println("wycheproof ecdsa_secp256k1_p1363: ${passed} ok, ${rejected_invalid} invalid rejected, ${failed} failed (stride ${stride})")
if failed > 0 { exit(1) }
println("ALL PASS")
}
Loading
Loading