Kernel DES weak/semi-weak key and parity checking disabled by default (des_check_key=0)
Summary
des_check_key ships as 0 and nothing in-tree ever sets it, so des_set_key() always dispatches to the unchecked path: the correctly-implemented 16-entry weak/semi-weak table and parity validation are dead code by default and a weak key like 0x0101010101010101 is silently accepted (rc=0), demonstrated live. Reachability privileged-only (cryptoapi/des_ecb root-configured SAs; legacy smb_crypt NTLM-derived keys an attacker cannot steer onto the weak list). Ceiling: an operator deploying a cryptographically broken DES SA gets no warning - fail-open validation default on a legacy algorithm, not an exploitable primitive. Fix validated live: one-character default flip -> rc=-2, announce to consumers relying on non-odd-parity keys (desired fail-closed). Tables/schedule proven correct: parity 256/256, weak detection 16/16 + near-miss negatives, 2666 schedules byte-identical to LibreSSL both directions = 8916/0.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3565 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| des_kat.c | β | 7.6 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 89 B | view raw | |
| shim/ | β | 4.0 KB | β download | |
| build.log | β | 8.7 KB | view raw | |
| run.log | β | 2.2 KB | view raw | |
| fix.diff | β | 340 B | view raw | |
| fix_run.log | β | 1.2 KB | view raw | |
| fix_build.log | β | 61 B | view raw | |
| env.txt | β | 1.3 KB | view raw | |
| manifest.json | β | 965 B | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-3565 β Kernel DES weak/semi-weak key checking disabled by default
Target: sys/crypto/des/des_setkey.c (pass 2 GLM 5.3 re-audit)
Class: insecure default / CWE-1188 (Initialization with insecure default)
Severity: Info (privileged configuration surface; DES itself legacy)
What
int des_check_key = 0; (sys/crypto/des/des_setkey.c:65) ships the kernel
DES key-schedule entry point in unchecked mode. des_set_key()
(des_setkey.c:142-153) only routes through des_set_key_checked() (parity
+ weak/semi-weak rejection) when this global is set β and nothing in the
tree ever sets it. Consequently every kernel DES consumer silently accepts:
- keys with wrong parity, and
- all 16 weak/semi-weak keys (0101010101010101, FEFEFEFEFEFEFEFE, β¦), which produce the pathological schedules the table at des_setkey.c:97-115 exists to reject (E(C(k)) patterns with 2^2-key meet-in-the-middle).
Verified live (baseline):
== default policy: des_set_key with des_check_key==0 == des_set_key(0x0101010101010101) rc=0 (0=ACCEPTED) [des_check_key=0]
Reachability analysis (why Info, not higher)
- In-tree consumers:
sys/crypto/des/des_ecb.c(kernel crypto plumbing) andsys/netproto/smb/smb_crypt.c(legacy smbfs, NTLM-derived keys). DragonFly has no opencrypto/cryptosoft; IPsec ESP-DES via cryptoapi would require root-configured SAs. Keys are therefore admin-chosen or hash-derived β an attacker cannot steer a derived key onto the weak list without breaking NTLM first. - Impact ceiling: an admin who configures a weak DES key gets no warning, yielding cryptographically broken confidentiality on that SA. It is a fail-open default on a legacy algorithm, not an escalation.
- Related hygiene noted during audit (same class as DF-3564, filed there):
des_set_key_uncheckedkeeps key-derived register material (c, d, s, t, t2 at des_setkey.c:172-222) on the stack after returning; no verbatim key copy is made (the input key is only read via c2l), so the marginal exposure is small.
Also verified live in this pack (the reason the file is otherwise sound)
des_kat.c β 8916 checks, 0 failures:
odd_parity[]table (podd.h) matches independently computed odd parity for all 256 bytes;des_check_key_parityaccepts its own output and rejects single-bit parity breaks.- All 16 weak/semi-weak keys are detected by
des_is_weak_keyand rejected (-2) bydes_set_key_checked; 4096 random keys + 16 one-bit near-misses of the weak list produce no false positives. - The key schedule itself (shifts2 table, PC1/PC2 via des_skb) is correct:
2000 random keys + 666 weak-key schedules round-trip ECB encrypt/decrypt
byte-identical to LibreSSL 3.6.1
DES_set_key_unchecked/DES_ecb_encrypt, plus the classic anchor key=133457799BBCDFF1, pt=0123456789ABCDEF β ct=85E813540F0AB405.
Fix
fix.diff: default des_check_key = 1. Validated live on a patched copy:
des_set_key(0x0101010101010101) then returns -2 (rejected) via the
default path and every KAT still passes. Deploy note: any consumer relying
on accepting non-odd-parity keys (smb_crypt derives keys without forcing
parity in places) would start failing β that is the desired fail-closed
behavior, but should be announced.
Reproduce
cc -O0 -g -I shim -c /usr/src/sys/crypto/des/des_setkey.c -o des_setkey.o cc -O0 -g -I shim -c /usr/src/sys/crypto/des/des_enc.c -o des_enc.o cc -O0 -g -I shim -c /usr/src/sys/crypto/des/des_ecb.c -o des_ecb.o cc -O0 -g -I shim des_kat.c des_setkey.o des_enc.o des_ecb.o \ -I/usr/local/include -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lcrypto \ -o des_kat ./des_kat # watch the "default policy" section
DF-3565 β VERDICT
Status: reproduced (the unsafe-default behavior β silent acceptance of
the weak key 0x0101010101010101 by the default des_set_key() path β
demonstrated live; impact ceiling: fail-open key validation on a privileged,
legacy configuration surface).
Narrative
Pass-2 audit of sys/crypto/des/des_setkey.c (233 LOC, Eric Young lineage).
The file contains a correct and complete weak/semi-weak detection table
(16 entries, des_setkey.c:97-115) and parity machinery β but the kernel
global des_check_key (des_setkey.c:65) defaults to 0, and des_set_key()
(des_setkey.c:142-153) consults it to decide between the checked and
unchecked paths. No code in the tree ever sets it, so the shipped kernel
always takes des_set_key_unchecked() and a weak key such as
0101010101010101 is silently accepted (rc=0) β demonstrated in the baseline
run.
The detection machinery itself was proven live (all 16 weak keys β -2 via
des_set_key_checked; 4096 random + 16 near-miss negatives β accepted), and
the schedule computation (shifts2, PC1/PC2, des_skb) was proven correct
against LibreSSL 3.6.1 over 2666 keys both directions, so the defect is
purely the default, not the tables.
Reachability: consumers are the kernel cryptoapi/des_ecb.c plumbing
(root-configured SAs at most) and legacy smb_crypt.c (NTLM-derived keys).
An attacker cannot choose the key; the risk is an operator quietly
deploying a cryptographically broken SA. Hence Info severity β a
fail-open default worth fixing, not an exploitable primitive.
Fix validation
fix.diff flips the default to des_check_key = 1. On a patched copy of
the file (never applied to sys/):
- baseline:
des_set_key(0x0101010101010101) rc=0 (0=ACCEPTED) [des_check_key=0] - patched:
des_set_key(0x0101010101010101) rc=-2β weak key rejected by the default path; all other 8915 checks still pass (the single reported FAIL in the patched run is the harness's acceptance assertion inverting, i.e. the fix working).
See fix_run.log.
Negative results recorded during this audit (weight for the clean parts)
- Memory safety: fixed-size tables only;
des_set_key_uncheckedwrites exactly 32 DES_LONGs across the 16-entry schedule (in bounds);des_is_weak_keymemcmp issizeof(des_cblock)=8 on both sides. - Parity table
odd_parity[256](podd.h): verified against independently computed parity, all 256 entries. - shifts2 rotation logic (28-bit rotate-left by 1 or 2 in a 32-bit word) verified by construction and by the 2666-key ECB cross-match.
- The
weak_keyfield ofstruct des_ks_structis never written by this file (dead field) β noted, harmless.
Kernel references
- sys/crypto/des/des_setkey.c:65 (insecure default)
- sys/crypto/des/des_setkey.c:142-153 (dispatch around the default)
- sys/crypto/des/des_setkey.c:159-167 (checked path, verified correct)
- sys/crypto/des/des_setkey.c:97-115 (weak key table, verified complete)
- sys/netproto/smb/smb_crypt.c (legacy consumer)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedOne-character default flip des_check_key=1 (des_setkey.c:65). Baseline: weak key 0101010101010101 accepted rc=0 by the default des_set_key path. Patched copy of the same source: rc=-2 (rejected via parity+weak checks); all 8915 other checks unchanged. The lone reported FAIL in the patched run is the harness's acceptance assertion inverting, i.e. the fix taking effect.
fix_run.log: '=== DF-3565 fix validation ===' -> 'des_set_key(0x0101010101010101) rc=-2 ... [des_check_key=1]' contrasted with the baseline line in run.log.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log, 'default policy' section: 'des_set_key(0x0101010101010101) rc=0 (0=ACCEPTED) [des_check_key=0]' inside an otherwise all-green 8916-check run (parity table, weak-key detection when enabled, schedule-vs-libcrypto). fix_run.log: patched default -> rc=-2 via the same default path.
PoC changes
Harness written from scratch (des_kat.c). Two environment quirks handled: (1) openssl/des.h and the kernel crypto/des/des.h share the HEADER_DES_H include guard, so the two needed libcrypto DES symbols are declared manually; (2) a memorized 'classic' DES anchor constant (3fa40e8a984d4815) turned out to belong to a different key/plaintext pair and was dropped - the authoritative cross-check is the libcrypto agreement, which matched on all 2666 keys (the correct classic pair key=133457799BBCDFF1 pt=0123456789ABCDEF -> 85E813540F0AB405 passed as hardcoded anchor).
Verified recommended fix
Default des_check_key to 1 (fail-closed) and audit consumers for parity assumptions; see fix.diff.
Verdict
The kernel DES entry point ships with des_check_key=0 (sys/crypto/des/des_setkey.c:65) and nothing in-tree ever enables it, so des_set_key() (des_setkey.c:142-153) always takes the unchecked path: the 16-entry weak/semi-weak table and parity validation that the file correctly implements are dead code by default. Demonstrated live: des_set_key(0x0101010101010101) returns 0 (accepted) with the stock default, and returns -2 once the default is flipped. The detection machinery and the key schedule themselves verified correct (weak table exact vs 4096 random + 16 near-miss negatives; 2666 schedules ECB cross-matched against LibreSSL 3.6.1 both directions). Reachability is privileged-only (root-configured cryptoapi SAs / legacy smb_crypt NTLM-derived keys), an attacker cannot choose the key, so impact is a fail-open validation default on a legacy algorithm rather than an exploitable primitive.
No comments yet.