lwIP-CE TLS Specification Compliance

Clause-by-clause ledger mapping the standards this TLS implementation targets (RFC, NIST SP, FIPS) to the code that satisfies or deviates from them. 271 entries.

243COMPLIANT
9DEVIANT
11NEEDS-EVIDENCE
8NOT-APPLICABLE
Showing all 271 entries

Deviation policy: Every DEVIANT result is expected to be fixed. If a deviation is intentionally retained because of platform constraints, its audit entry must include a specific <justification>...</justification> explaining why. The grade is the current status; a compliant entry's note may retain the original audit finding for traceability.

FIPS 180-4

SectionLocationGradeNoteJustification
1 / 5.1.1 (message length limit, streaming state)src/tls/core/sha256.s: _tls_sha256_update (L222-276), _tls_sha256_digest (L288-292) - 64-bit bitlen, non-destructive digestCOMPLIANTCompliant as of/before v3.0.0-rc1. bitlen is uint64 (no 2^64 overflow check, unreachable). Digest operates on a stack copy so the transcript context stays usable (hash.h L77-82); len==0 update is a no-op.
5.1.1 (padding)src/tls/core/sha256.s: _tls_sha256_digest (L280-346) - 0x80, zero fill, 64-bit big-endian lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. Both the one-block (datalen <= 55) and two-block padding branches are present. Covered by CAVP SHA256ShortMsg/LongMsg/Monte and tests/unit/tls_hash FIPS examples ("abc", "", 448-bit message).
6.2 (SHA-256 computation; 4.2.2 constants; 5.3.3 H(0))src/tls/core/sha256.s: _sha256_transform (L350-728), _sha256_state_init (L730-738), _sha256_k (L740-804) - compressionCOMPLIANTCompliant as of/before v3.0.0-rc1. Constants and compression are validated by the CAVP SHA-256 Monte vectors. The message schedule lives in static _sprng_sha_mbuffer (L18); see the ISO/IEC 19790 zeroization entry.

FIPS 186-5

SectionLocationGradeNoteJustification
5.4 (RSA public exponent: odd, 2^16 < e < 2^256)src/tls/core/rsa.c: tls_rsa_decrypt_signature (L303-316) - exponent sanityCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): only e != 0 and exp_len <= 3 bytes are enforced, so e=1 or an even e is accepted from a certificate SPKI. Not exploitable without a CA signing such a key. See also RFC 8017 3.1.
6.4.1 (ECDSA signing; nonce per RFC 6979; r=0 or s=0 means new k)src/tls/core/p256.s: _tls_p256_sign (.Lsg_retry path), p256_det_k_continue (new label after L3602)COMPLIANTFixed as of v3.0.0-rc2. FIXED: r==0 or s==0 now jumps to .Lsg_retry which calls p256_det_k_continue (new entry at .Ldk_gen with K/V state preserved), then repeats the scalar multiply per RFC 6979 §3.2 step h.3. Probability of needing the retry is ~2^-256. RFC 6979 A.2.5 vector continues to pass.
6.4.2 step 1 (r, s in [1, n-1])src/tls/core/p256.s: _tls_p256_verify (L3381-3397); src/tls/core/x509.c: tls_ecdsa_parse_int (L25-55) - r/s rangeCOMPLIANTCompliant as of/before v3.0.0-rc1. p256_import_be with C=1 rejects 0 and values >= n for both r and s; DER is strictly minimal, non-negative, at most 32 magnitude bytes. Covered by tests/unit/tls_p256 test_reject_r0/s0/rn/sn.
6.4.2 step 2-3 (hash to integer e)src/tls/core/p256.s: _tls_p256_verify (L3400-3406) - e derivationCOMPLIANTCompliant as of/before v3.0.0-rc1. The API fixes a 32-byte SHA-256 digest (hlen = N = 256) so no truncation is needed; e is reduced mod n.
6.4.2 step 4-6 (w = s^-1, u1, u2, R = u1*G + u2*Q)src/tls/core/p256.s: _tls_p256_verify (L3407-3413), p256_verify_shamir (L2856+) - double-scalar multiplyCOMPLIANTCompliant as of/before v3.0.0-rc1. Variable-time inverse and Straus/Shamir are acceptable because all inputs are public. Verified by the RFC 6979 A.2.5 vector plus tamper and off-curve negatives in tests/unit/tls_p256.
6.4.2 step 6 (R = O rejected)src/tls/core/p256.s: _tls_p256_verify (L3414-3424) - infinity rejectionCOMPLIANTCompliant as of/before v3.0.0-rc1. Rejects when vs_inf is set and separately when Z(R)==0, so the projective x-check cannot degenerate.
6.4.2 step 7-8 (v = x_R mod n; accept iff v == r)src/tls/core/p256.s: _tls_p256_verify (L3425-3453) - projective x(R) mod n == rCOMPLIANTCompliant as of/before v3.0.0-rc1. Checks X == r*Z^2 (mod p) and, only when r+n < p, X == (r+n)*Z^2; equivalent to x_R mod n == r without inversion.

FIPS 197

SectionLocationGradeNoteJustification
5.1 (Cipher; Nk/Nr combinations)src/tls/core/aes.s: _aes_encrypt_block (L217-373) - round count and cipherCOMPLIANTCompliant as of/before v3.0.0-rc1. Nr = keysize/32+6 (10/12/14). Covered by CAVP CBC GFSbox/KeySbox/VarKey/VarTxt/MMT for 128/192/256 and tests/unit/tls_aes SP 800-38A F.2.x.
5.1.1 (SubBytes implementation side channel; project policy)src/tls/core/aes.s: _aes_encrypt_block/.Lencrypt_subbytes (L310-319), _aes_key_schedule (L117-128), _gf128_mul (L377-492) - table-lookup timingCOMPLIANTCompliant as of/before v3.0.0-rc1. Byte S-box (no T-tables) on an eZ80 with no data cache; xtime and GF(2^128) reduction use branch-free masks; branches depend only on key size or loop counters. Power/EM leakage not assessed.
5.2 (KeyExpansion)src/tls/core/aes.s: _aes_key_schedule (L81-190); src/tls/core/aes.c: tls_aes_init (L354-365) - 128/192/256 expansionCOMPLIANTCompliant as of/before v3.0.0-rc1. Rcon doubling and the AES-256 SubWord at i mod 8 = 4 special case are present; only 16/24/32-byte keys accepted. Covered by CAVP KeySbox/VarKey for all sizes.
5.3 (InvCipher)src/tls/core/aes.s: _aes_decrypt_block (L519-678) - inverse cipherCOMPLIANTCompliant as of/before v3.0.0-rc1. Straightforward inverse cipher with InvSBox. Covered by CAVP CBC decrypt and tests/unit/tls_aes F.2.2/F.2.4/F.2.6.

FIPS 198-1

SectionLocationGradeNoteJustification
4 (key of any length; error handling)src/tls/core/hmac.c: tls_hmac_context_init (L27-38) - zero-length key, unchecked initCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): keylen==0 is rejected although FIPS 198-1 permits it (HKDF works around it, hkdf.c L67-70). In the keylen > 64 path the tls_hash_context_init return value is ignored (L34), so an unsupported algorithm calls uninitialized function pointers before the switch default rejects.
4 steps 1-3 (K0 derivation, ipad/opad); RFC 2104 2src/tls/core/hmac.c: tls_hmac_context_init (L25-71), tls_hmac_digest (L78-88) - key processingCOMPLIANTCompliant as of/before v3.0.0-rc1. keylen > 64 hashes the key first, otherwise zero-pads; ipad 0x36, opad 0x5C; outer hash computed on a copy. Covered by CAVP HMAC.rsp [L=32] and tests/unit/tls_hmac RFC 4231 cases.

ISO/IEC 19790

SectionLocationGradeNoteJustification
7.9.7 (SSP zeroisation)src/tls/contrib/x25519/src/x25519.s: .x25519.data (L779-807), _tls_x25519_secret epilogue (L123-139) - X25519 scratch residueCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: _clamped (the private scalar), ladder state _a.._f and _product (pre-normalisation shared secret) live in static .bss and are never wiped; handshake X25519 calls (handshake.c L1781-1797, L2760-2786) are not wrapped in tls_crypto_guard, unlike P-256. Fix: zero the 388-byte block before both returns and wrap the calls.
7.9.7 (SSP zeroisation)src/tls/core/aes.c: tls_aes_verify (L795-824), tls_aes_ccm_encrypt (L833-854), tls_aes_ccm_decrypt (L864-892), tls_aes_init (L391-393) - AES context residueCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (medium): tls_aes_verify memcpy's the whole context (round keys and GHASH key H) to stack tmp and never wipes it (the memset at L816 is commented out); leaking H enables GCM forgeries under that key. One-shot CCM leaves ctx on the stack; no tls_aes_clear API. The altcp record aes_ctx is also never wiped (see RFC 8446 7.1).
7.9.7 (SSP zeroisation)src/tls/core/bytes.s: _tls_secure_memzero (L34-53) - erase helperCOMPLIANTCompliant as of/before v3.0.0-rc1. External assembly routine (seed byte plus overlapping ldir) that the compiler cannot elide; len 0/1 handled. The bytes.h L27 comment ("Uses volatile") is inaccurate but harmless.
7.9.7 (SSP zeroisation)src/tls/core/hmac.c: tls_hmac_context_init (L30-31), tls_hmac_digest (L79-80) - sum[], htmp, tbufCOMPLIANTCompliant as of/before v3.0.0-rc1. Padded key, hashed key, inner digest and K0 xor ipad/opad contexts stay on the stack; no HMAC clear API. Some handshake.c callers use TLS_AUTOZERO_DECL_STRUCT, but the primitive itself does not wipe.
7.9.7 (SSP zeroisation)src/tls/core/p256.s: p256_di/p256_wipe/p256_exit (L3217-3263), _tls_p256_sign saferam wipe (L3692-3699); src/tls/core/handshake.c: tls_handshake_cleanup (L5045-5103) - P-256 and ephemeral key zeroizationCOMPLIANTCompliant as of/before v3.0.0-rc1. __p256_scratch is wiped on entry and every exit, _sprng_sha_mbuffer after RFC 6979, handshake P-256 calls are wrapped in crypto_guard, and the used private key and shared secret are zeroed right after ECDH. The unused group's private key survives until tls_handshake_cleanup (informational).
7.9.7 (SSP zeroisation)src/tls/core/passwords.c: tls_pbkdf2 (L27-30) - hash_buffer, hash_composite, HMAC contextsCOMPLIANTCompliant as of/before v3.0.0-rc1. Last T block (derived key material), Uc and three HMAC contexts holding password xor ipad/opad stay on the stack unwiped.
7.9.7 (SSP zeroisation)src/tls/core/rsa.c: tls_rsa_encrypt (L277), tls_rsa_decrypt_signature (L336); src/tls/core/share/bigint.s: scrub (L258-275); src/tls/core/key.c (L1076-1094) - RSA scratchCOMPLIANTCompliant as of/before v3.0.0-rc1. Top 2*mod_len bytes of shared scratch and relocated powmod slots/alternate registers are cleared after every powmod; __rsa_transient and OAEP tmp zeroed in key.c. tls_rsa_decode_oaep's own frame is scrubbed only when the caller wraps it in tls_crypto_guard.
7.9.7 (SSP zeroisation)src/tls/core/sha256.s: _tls_sha256_digest (L344), _sha256_transform (L726), _sha256_m_buffer=_sprng_sha_mbuffer (L18) - SHA-256 working state residueCOMPLIANTCompliant as of/before v3.0.0-rc1. The digest's 105-byte local context copy and the transform frame are not wiped; the static 256-byte W buffer is never wiped and after HMAC init holds K xor ipad. HMAC/HKDF/PBKDF2 call tls_sha256_* directly and bypass the crypto_guard scrub that tls_hash_update/digest get (hash.c L58-70).

ITU-T X.690

SectionLocationGradeNoteJustification
8.1.2 / 10.2 (exact tag identity: class and constructed bit)src/tls/core/x509.c: tls_x509_parse_certificate (L1212, L1322, L1373-1374), tls_x509_validate_path_extensions (L474-485), tls_x509_oid_eq (L80-86) - tag number compared without classCOMPLIANTCompliant as of/before v3.0.0-rc1. Many checks use only tls_asn1_tag_number(), so e.g. a context-specific [2] or a constructed OCTET STRING passes where INTEGER/primitive OCTET STRING is required. Low risk (content is CA-signed) but not strict DER; tls_cert_extract_sig_material and tls_x509_signature_algorithm compare raw tag bytes correctly.
8.1.2.4 (high-tag-number form)src/tls/core/asn1.c: tls_asn1_next (L78-80) - single-octet tag assumptionCOMPLIANTCompliant as of/before v3.0.0-rc1. A tag with low bits 0x1F is treated as tag 31 and its continuation octet is parsed as the length instead of being rejected. Parser differential inside signed content; limited impact.
8.1.3 (length vs. available content; bounds)src/tls/core/asn1.c: tls_asn1_parse_len / tls_asn1_next / tls_asn1_child_cursor (L29-32, L88-91, L100-113) - buffer boundsCOMPLIANTCompliant as of/before v3.0.0-rc1. Length octets and content length are checked against the cursor end (end - p < content_len, no size_t overflow); child cursors are confined to the parent value.
8.1.3.2 / 8.1.3.5 / 10.1 (definite length only, shortest form)src/tls/core/asn1.c: tls_asn1_parse_len (L5-50) - definite/minimal lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. Indefinite form rejected (L25), leading 0x00 length octet rejected (L33-36), long form encoding a value under 0x80 rejected (L42-45), too many length octets rejected (PR #63). tests/unit/tls_asn1 covers basic cases only; no negative vectors for non-minimal lengths.
8.2 / 11.1 / 11.5 (BOOLEAN DER value; DEFAULT FALSE omitted)src/tls/core/x509.c: tls_x509_validate_path_extensions (L477-483, L521-525) - critical / cA BOOLEANCOMPLIANTCompliant as of/before v3.0.0-rc1. Only 0x00/0xFF accepted (correct per 11.1), but an explicitly encoded DEFAULT FALSE (critical=FALSE or cA=FALSE) is accepted, violating 11.5. Lenient; no security effect.
8.3.2 (INTEGER minimal encoding)src/tls/core/x509.c: tls_ecdsa_parse_int (L25-55) - ECDSA r/sCOMPLIANTCompliant as of/before v3.0.0-rc1. Rejects negative values, zero, redundant leading 0x00 and long-form lengths; the SEQUENCE length must match sig_len exactly (L1830-1841).
8.3.2 (INTEGER minimal encoding)src/tls/core/x509.c: tls_x509_validate_path_extensions (L528-531), tls_x509_parse_certificate (L1371-1387) - pathLenConstraint / RSA modulus+exponent / serialCOMPLIANTCompliant as of/before v3.0.0-rc1. pathLen accepts a redundant leading 0x00; RSA modulus strips one leading 0x00 and does not reject a negative modulus; serial encoding is never checked. Low impact (signed content).
8.6.2 / 11.2.1 (BIT STRING unused-bits octet)src/tls/core/x509.c: tls_x509_parse_certificate (L1352, L1392); src/tls/core/handshake.c: tls_cert_extract_sig_material (L645-649) - SPKI and signatureValueCOMPLIANTCompliant as of/before v3.0.0-rc1. SPKI and signatureValue BIT STRINGs require 0 unused bits and length >= 2.
8.6.2.2 / 11.2.2 (BIT STRING padding bits zero; no trailing zero bits)src/tls/core/x509.c: tls_x509_validate_path_extensions (L536-543) - keyUsage BIT STRINGCOMPLIANTCompliant as of/before v3.0.0-rc1. Unused-bits count is range-checked (0..7) but padding bits are not checked to be zero and trailing zero bits are not rejected; a KU with unused=7 still reads keyCertSign from padding. Low impact (signed content).
8.19 (OBJECT IDENTIFIER)src/tls/core/x509.c: tls_x509_oid_eq / tls_x509_oid_to_sig_alg (L80-86, L1584-1614); src/tls/includes/tls_algorithms.h (L53-77) - OID matchingCOMPLIANTCompliant as of/before v3.0.0-rc1. OIDs are compared byte-for-byte against fixed DER constants (checked against dotted forms); a non-minimal subidentifier can only fail to match, which rejects.

NIST SP 800-108

SectionLocationGradeNoteJustification
4 (KDF in counter/feedback/double-pipeline mode)(none) - not implementedNOT-APPLICABLENo SP 800-108 KDF exists; the only KDFs are HKDF and PBKDF2.

NIST SP 800-131A Rev. 2

SectionLocationGradeNoteJustification
3 / Table 2 (RSA modulus at least 2048 bits for signature verification)src/tls/core/handshake.c: tls_cert_walker_validate_one (L857-869); src/tls/core/rsa.c: tls_rsa_decrypt_signature (L302-303) - modulus size lower boundCOMPLIANTCompliant as of/before v3.0.0-rc1. Minimum enforced on byte length (mod_len >= 256 after stripping one leading 0x00), not bit length, so a 2041-2047-bit modulus encoded in 256 bytes passes. Low risk; fix by requiring a nonzero top byte or counting bits.

NIST SP 800-132

SectionLocationGradeNoteJustification
5.1 (salt: random portion at least 128 bits)src/tls/core/passwords.c: tls_pbkdf2 (L23) - salt lengthCOMPLIANTFixed as of v3.0.0-rc2. Only saltlen==0 is rejected. Only caller is PKCS#8 import (key.c L484) with file-supplied salt; defensible for decrypt-only interop, but a generation path would need the minimum. Salt length is now enforced >= 16 bytes (key.c, after PBKDF2-params parse).
5.2 (iteration count; minimum 1000 recommended)src/tls/core/passwords.c: tls_pbkdf2 (L25, L59) - iteration count; src/tls/core/key.c: tls_key_import_pkcs8 - rounds capCOMPLIANTFixed as of v3.0.0-rc2. FIXED: tls_key_import_pkcs8 now rejects rounds==0 or rounds>600000 with TLS_KEY_IMPORT_DECRYPT_FAIL before calling tls_pbkdf2. Cap of 600,000 is above the OWASP-recommended minimum (210,000 for PBKDF2-SHA256) and below the CPU-DoS threshold on a 48 MHz eZ80.

NIST SP 800-186

SectionLocationGradeNoteJustification
3.2.1.3 (P-256 domain parameters)src/tls/core/p256.s: .rodata.__p256_const (L246-328) - curve constantsCOMPLIANTCompliant as of/before v3.0.0-rc1. p, n, n-1, n-2, 2n and Montgomery-form b, Gx, Gy, R, R^2 mod p and R^2 mod n were recomputed in Python and match byte for byte.

NIST SP 800-38A

SectionLocationGradeNoteJustification
6.2 (CBC mode chaining)src/tls/core/aes.c: tls_aes_encrypt CBC (L664-676), tls_aes_decrypt CBC (L747-769) - chainingCOMPLIANTCompliant as of/before v3.0.0-rc1. Correct chaining; in-place decrypt safe; decrypt rejects non-multiple-of-16 lengths. Covered by CAVP CBC MMT and SP 800-38A F.2.1-F.2.6. outbuf==NULL is not checked in CBC decrypt (UB on misuse).
6.5 (CTR mode exposure)src/tls/core/aes.c: tls_aes_gcm_ctr (L706-712) - raw GCTR exportCOMPLIANTCompliant as of/before v3.0.0-rc1. Only raw CTR exposure is GCTR on a GCM context; its only user (altcp_tls_ce.c L480) runs after tag verification. Missing lock check: see SP 800-38D 7.1/7.2 ordering entry.
Appendix A (padding; project intends PKCS#7)src/tls/core/aes.c: tls_aes_encrypt CBC final block (L697-707) - padding block; src/tls/core/key.c: tls_key_encrypt CBC ct_len calc (L906-910) - always include padding blockCOMPLIANTFixed as of v3.0.0-rc2. FIXED: padding loop uses iv[idx] correctly. Block-aligned input previously got no padding block (ct_len was in_len when rem==0); both aes.c (removed the `if (rem)` guard — padding block is always written) and key.c (ct_len now always in_len - rem + AES_BLOCK_SIZE regardless of rem) are corrected.
Appendix C (unpredictable CBC IV)src/tls/core/aes.c: tls_aes_init (L367); src/tls/core/key.c (tls_random_bytes(iv), ~L908) - CBC IV sourceCOMPLIANTCompliant as of/before v3.0.0-rc1. The only CBC encrypt caller draws the IV from the DRBG after tls_rng_healthcheck(). No padding oracle: tls_aes_decrypt does not check padding and the PKCS#8 strip (key.c L518-521) is offline import.

NIST SP 800-38C

SectionLocationGradeNoteJustification
6.1 / 6.2 (Tlen parameter integrity)src/tls/core/aes.c: tls_aes_ccm_encrypt (L841, L852), tls_aes_ccm_decrypt (L873, L884) - size_t to uint8_t tag_len truncationCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (medium, API misuse): tag_len is validated after a cast to uint8_t (260 becomes 4) but the original size_t is used for memcpy, so encrypt over-reads the 16-byte stack digest (adjacent ctx holds round keys) into the caller's tag. Current callers pass the constant TLS_KEY_AES_TAG_LEN. Fix: reject tag_len > 16 before the cast.
6.2 (decryption-verification: return INVALID only)src/tls/core/aes.c: tls_aes_ccm_decrypt (L857-893) - plaintext release on tag failureCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (medium): plaintext is written before verification and wiped on mismatch only if plaintext != ciphertext (L888-889), so an in-place call leaves unverified plaintext in the buffer. Tag compare is constant-time (tls_bytes_compare, L884).
6.2 (streaming decryption-verification)src/tls/core/aes.c: ccm_process_message (L203), tls_aes_verify (L832)COMPLIANTCompliant as of/before v3.0.0-rc1. ccm_process_message guards outbuf writes behind `if (outbuf)` (L203); tls_aes_verify passes NULL, so no plaintext is emitted during the tag-check pass. The API-level hazard exists only if a caller passes a live output buffer before verifying — no in-tree caller does this.
Appendix A.1 (Nlen 7..13, Tlen in {4,6,...,16}, Q < 2^(8q))src/tls/core/aes.c: ccm_validate_params (L67-83) - parameter limitsCOMPLIANTCompliant as of/before v3.0.0-rc1. Nonce 7-13, even tag 4-16, q range and message length bound enforced. Covered by CAVP CCM VNT/VTT/VPT/VADT/DVPT (128/192/256).
Appendix A.2.1 / A.2.2 (B0 formatting, AAD encoding)src/tls/core/aes.c: tls_aes_ccm_init (L430-433), ccm_update_aad (L98-162) - B0/AAD formattingCOMPLIANTCompliant as of/before v3.0.0-rc1. Flags and Q encoding correct; AAD length 2/6/10-byte encodings; AAD must be supplied in one call matching aad_len_total (fails closed otherwise). Covered by CAVP CCM and RFC 3610 packet vectors #1/#2 (tests/unit/tls_aes).
Appendix A.3 / 6.1 step 10 (counter blocks, T xor S0)src/tls/core/aes.c: ccm_prepare_counter (L164-172), tls_aes_ccm_init (L435-437), tls_aes_digest CCM (L501-514) - CTR/S0COMPLIANTCompliant as of/before v3.0.0-rc1. S0 = E(Ctr0), data from Ctr1, tag = MAC xor S0 truncated; digest fails closed if AAD or message totals are incomplete (L503-506).

NIST SP 800-38D

SectionLocationGradeNoteJustification
5.2.1.1 (96-bit IV: J0 = IV || 0^31 || 1)src/tls/core/aes.c: aes_gcm_prepare_iv (L297-329), tls_aes_init (L376-386) - 96-bit IV pathCOMPLIANTCompliant as of/before v3.0.0-rc1. Covered by CAVP gcmEncryptExtIV/gcmDecrypt (IVlen=96) and tests/unit/tls_aes McGrew-Viega TC4.
5.2.1.1 (input length limits; GHASH length block)src/tls/core/bytes.s: _bytelen_to_bitlen (L59-83), used by src/tls/core/aes.c: tls_aes_digest (L491-492) - [len(A)]64 || [len(C)]64COMPLIANTCompliant as of/before v3.0.0-rc1. BUG (medium): after the x8 shift, bit-length bits 16-23 are never stored, so any AAD or ciphertext of 8192 bytes or more gets a wrong length block (8192 bytes encodes as 0). GCM tags are wrong for TLS records of 8 KiB or more (inbound 16 KiB records fail closed; outbound records of 8 KiB or more get bad tags) and the encoding is not injective. CAVP GCM vectors are at most 408 bits, so untested. Fix: store all of A/HLU/H/L into bytes 4-7.
5.2.1.1 (IV length at least 1 bit)src/tls/core/aes.c: tls_aes_init (L348-352) - zero-length IVCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): iv_len==0 is accepted for GCM. key.c L1106 guards its own caller, but the PKCS#8 import path (key.c L500) and the primitive do not fail closed.
5.2.1.1 (length accumulators)src/tls/core/aes.c: tls_aes_update_aad (L451-452), tls_aes_update_ciphertext (L541-542) - size_t accumulatorsCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): 24-bit aad_len/ct_len accumulate across streaming calls with no overflow check and wrap at 16 MiB. The 2^39-256 bound is unreachable, but the accumulators should fail closed.
5.2.1.1 / 7.1 step 2 (non-96-bit IV via GHASH)src/tls/core/aes.c: aes_gcm_prepare_iv (L309-328), tls_aes_init (L348-352) - non-96 IV pathNEEDS-EVIDENCECode inspection shows a correct J0 = GHASH(IV || pad || [len(IV)]64) for 1-16 byte IVs, but cavp_fetch.py filters out every IVlen != 96 vector. Reachable from PKCS#8 import (key.c L500).
5.2.1.2 (tag length)src/tls/core/aes.c: tls_aes_digest (L470-500), tls_aes_verify (L817-819) - GCM tag lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. Only 128-bit tags produced and verified (allowed subset). CAVP vectors filtered to Taglen=128.
6.5 (GCTR inc32, keystream continuity)src/tls/core/aes.c: gcm_ctr_xor (L570-618), increment_iv (L48-59) - CTR keystreamCOMPLIANTCompliant as of/before v3.0.0-rc1. inc32 on bytes 12-15; partial-block keystream carried across chunks. Chunked CTR previously validated 3000/0 against PyCryptodome (not re-run in this audit). 2^32-block wrap unguarded but each record re-initializes the context.
7.1 / 7.2 (A then C ordering; state machine)src/tls/core/aes.c: tls_aes_update_aad (L443-462), tls_aes_update_ciphertext (L518-544), tls_aes_gcm_ctr (L706-712) - lock/op_assocCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): AAD after ciphertext is rejected, but update_ciphertext does not reject use after digest or on an encrypt-associated context, tls_aes_gcm_ctr checks neither lock nor op_assoc, and update_aad does not NULL-check. Misuse silently corrupts state instead of failing closed.
7.2 (GCM-AD must not output P on FAIL; streaming API)src/tls/core/aes.c: tls_aes_decrypt GCM branch (L764)COMPLIANTCompliant as of/before v3.0.0-rc1. tls_aes_decrypt GCM branch guards the CTR-XOR behind `if (outbuf)` (L764); tls_aes_verify passes NULL for the tag-check pass and never emits plaintext. The API-level hazard exists only for a caller that passes a live output buffer — no in-tree caller does this.
7.2 step 6 (T = T' constant-time comparison)src/tls/core/bytes.s: _tls_bytes_compare (L7-28) - constant-time equalityCOMPLIANTCompliant as of/before v3.0.0-rc1. OR-accumulates a xor b over every byte with no early exit and a branch-free 0/1 result. Hazard: len==0 wraps BC and compares 2^24 bytes; no current caller passes 0.
7.2 steps 5-7 (release P only if T = T')src/tls/core/aes.c: tls_aes_verify (L786-825) - verify-on-copy pathCOMPLIANTCompliant as of/before v3.0.0-rc1. Tag recomputed on a context copy and compared with tls_bytes_compare; aes.h L198-199 requires decrypt only after verify. Callers comply (key.c L503-509, L1117-1120; altcp_tls_ce.c L415-444 two-pass). CAVP gcmDecrypt FAIL vectors included.
8.2 (IV construction / uniqueness)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_build_aead_nonce (L233-241); src/tls/core/handshake.c: tls_build_aead_nonce (L356-364); src/tls/core/key.c (~L908) - caller IV generationCOMPLIANTCompliant as of/before v3.0.0-rc1. TLS record nonce is static_iv xor 64-bit sequence number (8.2.1 deterministic); key.c cipher API uses DRBG-random IVs (8.2.2).
8.3 (invocation limits)src/tls/core/key.c (random-IV GCM encrypt, ~L908-937) - per-key invocation countNEEDS-EVIDENCERandom 96-bit IVs limit a key to 2^32 invocations and nothing counts invocations per key in the key.c API. The TLS record path is covered by RFC 8446 5.5 limits (see that entry).

NIST SP 800-56A Rev. 3

SectionLocationGradeNoteJustification
5.6.1.2 (approved RBG with at least 128-bit strength)src/tls/core/handshake.c: tls_handshake_init (L1781-1803) - RBG backing ECDH keygenDEVIANTRequested strength is 128 bits (correct for P-256/X25519), but the DRBG has no CAVP/KAT coverage (tests/profiling/tls_cavp/README.md: "DRBG is intentionally NOT included") and the entropy estimate is unvalidated (see SP 800-90B 3.1.5).Platform-specific deviation. The TI-84 Plus CE provides no isolated execution environment or tamper-resistant storage. The ACE DRBG state lives in application RAM shared with the OS; there is no hardware mechanism to prevent arbitrary code from overwriting it and substituting a compromised state. Implementing the strength attestation required by 5.6.1.2 in this threat model would add documentation overhead without meaningful assurance benefit, as a compromised device can already bypass any in-process check. The underlying primitives (HMAC-SHA-256 DRBG, X25519, P-256) provide 128-bit classical security; the deviation is in formal attestation, not in the actual cryptographic strength.
5.6.1.2 / 5.7.1.2 (constant-time P-256 scalar multiplication)src/tls/core/p256.s: p256_scalar_mul (L2281-2513), p256_scalar_mul_ml (L2210-2278), p256_point_cswap/select (L677-713), p256_reduce_top/addback (L566-610) - constant-time ladderCOMPLIANTCompliant as of/before v3.0.0-rc1. Co-Z Montgomery ladder; scalar bit used only as cswap/select mask; exceptional k handled by constant-time select; branches only on loop counters and public values (inspection). Evidence: RFC 5903 KAT (tests/unit/tls_p256) and tests/profiling/tls_timing; the co-Z correctness model is not committed (header L27-29).
5.6.1.2.2 (key-pair generation by testing candidates)src/tls/core/handshake.c: tls_handshake_init (L1799-1820); src/tls/core/p256.s: _tls_p256_publickey (L3295-3301) - private scalar in [1, n-1]COMPLIANTCompliant as of/before v3.0.0-rc1. 32 DRBG bytes accepted only if 1 <= d <= n-1 (uniform). An out-of-range candidate (about 2^-32) aborts handshake init instead of retrying (availability only).
5.6.2.3.3 (full public-key validation, nQ = O)src/tls/core/p256.s: p256_point_import (L3144-3188) - order checkCOMPLIANTCompliant as of/before v3.0.0-rc1. No explicit nQ=O check; P-256 cofactor h=1 makes partial validation equivalent to full validation (equivalence argument, not an explicit check).
5.6.2.3.4 (partial public-key validation)src/tls/core/p256.s: p256_point_import (L3144-3188) - peer and verify-key point validationCOMPLIANTCompliant as of/before v3.0.0-rc1. Requires 0x04 prefix, x < p, y < p, and y^2 == x^3 - 3x + b; used by _tls_p256_secret and _tls_p256_verify. Covered by tests/unit/tls_p256 test_reject_offcurve (no x >= p / y >= p test).
5.7.1.2 (ECC CDH: P = O is an error; Z = x_P)src/tls/core/p256.s: _tls_p256_secret (L3321-3359), p256_affine_export (L3100-3142); src/tls/core/handshake.c: tls_recv_server_hello (L2788-2818) - shared point and secretCOMPLIANTCompliant as of/before v3.0.0-rc1. affine_export fails on Z==0; handshake zeroes secret and private key and sends fatal illegal_parameter. Only the 32-byte big-endian x-coordinate is output. RFC 5903 8.1 vectors in tests/unit/tls_p256.

NIST SP 800-90A Rev. 1

SectionLocationGradeNoteJustification
8.4 (security strength)src/tls/core/random.c: defines (L22-57), tls_rng_available_strength (L949-954); src/tls/core/handshake.c: tls_handshake_init (L1762-1806) - claimed 128/192-bit strengthNEEDS-EVIDENCESeed-size arithmetic supports the 128/192 claims, but the credited bits rest on the unvalidated H_min = 0.269 (see SP 800-90B 3.1.5).
8.6.3 / 9 (state reuse across reset or snapshot)src/tls/core/random.c: g_tls_drbg (L121-127), tls_rng_cleanup (L1311-1318) - persistenceCOMPLIANTCompliant as of/before v3.0.0-rc1. DRBG state lives only in RAM, is never persisted (PSK appvar holds no RNG state), is zeroized at cleanup and rebuilt from fresh entropy on every tls_init. Emulator snapshot/memory-read attackers explicitly out of scope (random.h L105-107).
8.6.7 (nonce)src/tls/core/random.c: tls_drbg_entropy_gather_step (L637-672), compile-time checks (L44-54) - nonceCOMPLIANTCompliant as of/before v3.0.0-rc1. 128-bit instance: one 192-bit conditioned block as 128-bit entropy plus 64-bit nonce (3s/2 in one call, permitted). 192-bit instance: independent 24-byte nonce. Conditional on the SP 800-90B entropy credit.
9.1 / 9.3.1 (prediction resistance)src/tls/core/random.c: tls_random_bytes_ex (L961-1035) - prediction_resistance_flagNOT-APPLICABLEPrediction resistance is neither supported nor claimed. Opportunistic reseeding must not be described as PR because output is still produced when the reserve is empty (L679-683).
9.2 (reseed trigger)src/tls/core/random.c: tls_drbg_apply_entropy_reserve (L676-696), tls_random_bytes_ex (L989-1002) - reseed schedulingCOMPLIANTCompliant as of/before v3.0.0-rc1. A full 192-bit reserve is applied before every generate once READY_192 and wiped afterwards; READY_128 has no reseed path, acceptable because the 2^48 interval is unreachable.
9.3 (error handling, blocking request loop)src/tls/core/random.c: tls_request_random_bytes (L1059-1099) - loop terminationCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): the loop exits only on success or state FAILED. If generate/reserve-apply keeps failing while READY_192, or the source repeatedly fails after READY_128 (each restart grants a fresh 15 s deadline), it never ends. Fix: bounded deadline or latch FAILED on generate error.
9.3 (error state; fail-closed before seeding)src/tls/core/random.c: tls_random_bytes_ex (L966-1034), tls_rng_fail (L528-535) - generate before ready / after failureCOMPLIANTCompliant as of/before v3.0.0-rc1. Insufficient strength returns NULL immediately (L984-988) with the destination zeroized (L1032-1034); FAILED zeroizes the DRBG and is terminal until restart. random.h L99-101 comment ("synchronously drives source initialization") is stale.
9.3 (readiness deadline time base)src/arch/sys_arch.c: sys_now (L14-19) - time base for random.c tls_rng_deadline_expired (L504-508) and lwIP.c lwip_tls_rng_wait_for_ready (L374-388)COMPLIANTCompliant as of/before v3.0.0-rc1. Regraded this audit against an uncommitted working-tree fix (previously: 32-bit clock()*1000/32768 overflow, ~131 s wrap). New split computation is exact floor(t*1000/32768), overflow-free and monotonic (CLOCKS_PER_SEC = 32768UL confirmed in toolchain time.h). Residual (low, fails safe): sys_now wraps at 131,072,000 rather than 2^32, so a deadline armed in the last 15 s before the ~36.4 h timer wrap never expires and wait_for_ready can time out early. Suggest a static assert on CLOCKS_PER_SEC.
9.3 (wait-for-ready helper, commit db1dcddca)src/lwIP.c: lwip_tls_rng_wait_for_ready (L374-388) - bounded waitCOMPLIANTCompliant as of/before v3.0.0-rc1. Bounded by timeout_ms with wrap-safe subtraction, returns false on FAILED, pumps lwip_service_events. Minor: waits for 192 bits though TLS sockets gate on 128 (lwIP.c L1520); spins the full timeout if the RNG was never started; affected by the sys_now wrap.
9.4 / 8.3 (uninstantiate, internal state protection)src/tls/core/random.c: tls_drbg_reset (L175-181), tls_hash_df (L309), tls_sha256_buf (L277); src/tls/core/sha256.s: _tls_sha256_digest - residual state copiesCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): V and C are zeroized correctly, but the SHA contexts inside Hash_df/Hashgen are unwiped stack locals and the static message schedule keeps the last block. The residue 0x03||V_prev lets the last output (e.g. the P-256 private key, last draw at handshake.c L1799) be recomputed until overwritten. Fix: zeroize hctx and the digest frame/m_buffer.
10.1.1.1 (internal state V, C, reseed_counter; seedlen 440)src/tls/core/random.c: struct tls_hash_drbg_state (L112-119) - internal stateCOMPLIANTCompliant as of/before v3.0.0-rc1. 55-byte V and C, uint64 reseed_counter. Evidence is a 300-case host differential test against an independent SP 800-90A model (0 mismatches) - NOT CAVP: tests/profiling/tls_cavp run_drbg_sha256 (main.c L485) is a stub.
10.1.1.2 (instantiate)src/tls/core/random.c: tls_hash_drbg_instantiate (L372-399) - V = Hash_df(seed), C = Hash_df(0x00||V), counter = 1COMPLIANTCompliant as of/before v3.0.0-rc1. Matches the spec (make_c L358-370); instance reset on failure. Differential test only (no CAVP).
10.1.1.2 / 8.7.1 (personalization string)src/tls/core/random.c: tls_drbg_entropy_gather_step (L637-672) - personalization_stringCOMPLIANTCompliant as of/before v3.0.0-rc1. No personalization input (optional). 8.7.1 recommends one; a device-unique value (calculator ID) would be cheap defence against cloned state.
10.1.1.3 (reseed)src/tls/core/random.c: tls_hash_drbg_reseed (L401-430) - V = Hash_df(0x01||V||entropy)COMPLIANTCompliant as of/before v3.0.0-rc1. Matches the spec with 192 bits of entropy per reseed (L55-57); instance reset if make_c fails. Covered by the reseed branch of the differential test.
10.1.1.4 / Table 2 (max_number_of_bits_per_request 2^19)src/tls/core/random.c: tls_drbg_generate_bytes (L473), tls_random_bytes_ex (L1004-1016), tls_rng_request_try_complete (L721-723) - request sizeCOMPLIANTCompliant as of/before v3.0.0-rc1. Cap is 65536 bytes; larger requests are split into separate generate calls with their own state update.
10.1.1.4 step 1 (reseed_counter > reseed_interval)src/tls/core/random.c: tls_drbg_generate_bytes (L472-475), tls_random_bytes_ex (L989-999) - reseed intervalCOMPLIANTCompliant as of/before v3.0.0-rc1. Interval 2^48 (L59); generation refused above it unless a 192-bit reserve can reseed.
10.1.1.4 step 2 (additional_input)src/tls/core/random.c: tls_drbg_generate_bytes (L463-496) - additional inputCOMPLIANTCompliant as of/before v3.0.0-rc1. additional_input is not supported (optional); null-input path only.
10.1.1.4 step 3 (Hashgen)src/tls/core/random.c: tls_hash_drbg_hashgen (L432-461) - data = V; w_i = Hash(data); data += 1COMPLIANTCompliant as of/before v3.0.0-rc1. Increment mod 2^440 (L347-356), last block truncated; multi-block outputs matched the differential model.
10.1.1.4 step 4-6 (V update)src/tls/core/random.c: tls_drbg_generate_bytes (L479-487) - H = Hash(0x03||V); V = V+H+C+reseed_counterCOMPLIANTCompliant as of/before v3.0.0-rc1. Big-endian add mod 2^440 (L330-345) with right-aligned addends; counter added before increment as required.
10.3.1 (Hash_df)src/tls/core/random.c: tls_hash_df (L299-328) - counter / no_of_bits_to_return encodingCOMPLIANTCompliant as of/before v3.0.0-rc1. Hash(counter || 0x000001B8 || input), counter from 0x01, truncated to 55 bytes; digest scratch zeroized. Differential test only.
11.3 (health testing: KAT of instantiate/generate/reseed)src/tls/core/random.c: tls_rng_drive_initialization (L907-942), tls_rng_healthcheck (L893-905) - DRBG self-testDEVIANTNo known-answer self-test runs before first use; tls_rng_healthcheck only checks a non-NULL source pointer and a one-bit mask. The CAVP DRBG slot is a stub (tests/profiling/tls_cavp/src/main.c L485).Platform-specific deviation. The TI-84 Plus CE provides no isolated execution environment or tamper-resistant storage. Exposing a mechanism to inject known parameters into a pre-first-use self-test would create a pathway for arbitrary code to supply a passing vector while substituting a compromised DRBG state — a vulnerability that does not exist on platforms with a secure boot chain or memory isolation. The production-path health testing (RCT, APT, source qualification) runs on actual entropy samples and cannot be bypassed this way. Implementing a KAT in this threat model adds attack surface without meaningful assurance benefit.

NIST SP 800-90B

SectionLocationGradeNoteJustification
3.1.5 / 3.1.5.1.2 (entropy estimate and conditioning credit)src/tls/core/random.c: TLS_ENTROPY_HMIN_Q16 (L33-43); src/tls/core/random.s: _tls_entropy_gather192 (L642-695) - h_in per 952-sample blockNEEDS-EVIDENCEH_min = 0.269 bits/sample is self-described as a "Provisional engineering floor ... Replace after formal non-IID assessment"; no 6.3 estimator output exists. tests/host/tls_random/test_health.py only re-checks the arithmetic.
3.1.5.1.1 (vetted conditioning component)src/tls/core/random.s: _tls_entropy_gather192 (L668-693), _tls_entropy_collect (L1482-1544) - SHA-256 over 119-byte pool truncated to 192 bitsCOMPLIANTCompliant as of/before v3.0.0-rc1. SHA-256 is vetted; n_in 952, n_out 192; overlay zeroized after each copy and destination zeroized on failure. Conditional on the 3.1.5 estimate.
3.2.2 (noise source documentation)src/tls/core/random.s: _tls_random_init_entropy (L29-240) - floating-bus/SRAM bit at 0xD65800..+512NEEDS-EVIDENCEOnly a header comment ("sense-amplifier noise"); no physical model, independence analysis or restart data. Unmapped reads may echo bus traffic (LCD DMA, USB) that an attacker could predict or influence.
4.3 (failure handling / persistent failure)src/tls/core/random.s: .Lrandom_health_fail (L697-714), .Lhealth_recover (L1419-1475); src/tls/core/random.c: tls_drbg_entropy_gather_step (L602-623) - failure notification and recoveryNEEDS-EVIDENCEPool wiped and epoch bumped on failure; recovery switches to a backup tap after retest, but the permitted rescan can re-select the tap that just failed and nothing distinguishes intermittent from persistent failure.
4.3 (start-up tests: 1024 consecutive samples)src/tls/core/random.s: .Linit_validate_candidate (L246-361) - start-up samplesDEVIANTTwo 952-sample windows with continuous RCT/APT state, discarded and never credited, but a ~520-byte wipe runs between them so the samples are not the at least 1024 consecutive samples 4.3 requires.Window size per collection is 952 bits. We scale the threshholds to this window size.
4.4.1 (Repetition Count Test)src/tls/core/random.s: .Lhc_sample (L1130-1166), .Lsample_health_check (L826+), cutoff (L96) - RCTCOMPLIANTCompliant as of/before v3.0.0-rc1. C = 1 + ceil(20/0.269) = 76 for alpha 2^-20; runs on every sample before conditioning with persistent state. Boundaries checked in tests/host/tls_random/test_health.py L129-131.
4.4.2 (Adaptive Proportion Test)src/tls/core/random.s: .Lhealth_apt (L1168-1213), parameters (L93-98) - APT window/cutoffDEVIANT4.4.2 requires W = 1024 for binary sources; code uses W = 952 (cutoff 844 is correct for W=952; would be 906 for W=1024). Small security impact.Window size per collection is 952 bits. We scale the threshholds to this window size.

NIST SP 800-90C

SectionLocationGradeNoteJustification
4 (RBG2 construction: instantiate with 3s/2, reseed with at least s)src/tls/core/random.c: tls_drbg_entropy_gather_step (L637-672), defines (L29-57) - seeding constructionNEEDS-EVIDENCERBG2-like structure (physical source, no PR) with correct seed sizes, but every 90C claim depends on a 90B-validated source, which does not exist.
Full-entropy definition (h_in at least n_out + 64)src/tls/core/random.c: compile-time checks (L41-46) - conditioning marginCOMPLIANTCompliant as of/before v3.0.0-rc1. A compile-time #error enforces credited raw bits >= 192 + 64; inherits the unvalidated H estimate.

Project policy

SectionLocationGradeNoteJustification
altcp recv/recved re-entrancy (RX_BUSY guard removal; not an RFC clause)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_recved (L2257-2301), altcp_tls_ce_pass_rx_data (L1727-1786) - nested delivery from app recv callbackNOT-APPLICABLEBUG (non-crypto robustness regression in the uncommitted working tree): with RX_BUSY removed, an app calling altcp_recved() inside its recv callback re-enters handle_rx_appldata and delivers record N+1 before N returns; a non-OK return then loses or reorders plaintext, and a nested abort leads to use-after-free or a fatal LWIP_ASSERT. Restore the guard or make delivery iterative.
crypto_guard interrupt masking nestingsrc/tls/core/share/crypto_guard.s: _tls_crypto_guard_enable (L27-44), .Lrestore_interrupts (L91-100) - IFF save/restoreCOMPLIANTCompliant as of/before v3.0.0-rc1. 16-level LIFO of IFF2 with saturation; ei only if the popped level was enabled. Coverage gap: HMAC/HKDF/PBKDF2/AES and X25519 callers do not use the guard.
crypto_guard stack scrub bounds (OS stack guard 0xD19881)src/tls/core/share/crypto_guard.s: _tls_crypto_guard_disable (L47-100), scrub (L63-88), stackBot equ (L18) - scrub floorCOMPLIANTCompliant as of/before v3.0.0-rc1. The known overrun is fixed: floor is stackBot+4 = 0xD19882 and the routine skips when SP-1 is below the floor or length is under 3. Assumes SP is on the OS stack.
Documentation accuracy (fail-closed claims)src/tls/includes/truststore.h (L10-13); src/tls/core/truststore.c (L52-54); src/tls/core/handshake.c (L493-500, L665-668) - stale commentsCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (doc): comments say unanchored chains are accepted and unsupported signature operations "warn and proceed temporarily", but the code rejects both (handshake.c tls_cert_chain_verify_one, tls_recv_certificate_streamed). Fix the comments (and handshake.h L9-14) so nobody restores the old fail-open behaviour.
Overlay 0xE30800 relocated-code hygienebuild-tools/meta/linker_script_lwip.ld: OVERLAY 0xE30800 (L171-233); src/tls/contrib/x25519/src/x25519.s: _x25519_reloc_run_start..end (L360-777); src/tls/core/share/bigint.s: scrub (L258-275) - cursorImage relocated-code residueCOMPLIANTCompliant as of/before v3.0.0-rc1. All operations that use the 0xE30800 overlay (X25519, P-256 ECDH/sign, RSA powmod) are wrapped by tls_crypto_guard_enable()/disable() at every call site in handshake.c, and additionally inside p256.s and hash.c. The guard scrubs the overlay window and SHA-256 stack frames below SP on return, ensuring no key material persists in the shared safe-RAM region between operations.
Shared helpers / scratchsrc/tls/core/share/helpers.s (L10-63), share/memory.s (L1-21), scratch.inc, virtuals.inc - shared helpers/scratchNOT-APPLICABLENo security-relevant crypto logic. __tls_scratch (1647 B) is shared by P-256, powmod, X25519 and HKDF; mutual exclusion is documented, not enforced.
Truststore entry iteration boundssrc/tls/core/truststore.c: tls_truststore_lookup (L263), tls_truststore_lookup_by_subject (L286-306) - entry_count off-by-oneCOMPLIANTCompliant as of/before v3.0.0-rc1. ++count > entry_count after the compare lets entry_count+1 entries be examined; entry bounds checks keep it within db_len, so informational only.
Truststore integrity (signed anchor set)src/tls/core/truststore.c: tls_truststore_init (L86-196), tls_truststore_entry_current (L198-203) - store signature and anchor validityCOMPLIANTCompliant as of/before v3.0.0-rc1. RSA-2048 PSS-SHA256 over version||timestamp||count||entries with an embedded key; SIG_INVALID makes tls_init fail (tls.c L326-333). Anchor validity windows enforced on lookup. No rollback protection (an older validly-signed store can be replayed); only an age warning.
Truststore readiness / fail-closedsrc/tls/core/tls.c: tls_cleanup (L343-356) - truststore_valid_for_session not resetCOMPLIANTCompliant as of/before v3.0.0-rc1. tls_cleanup does not reset truststore_valid_for_session, but no path reaches a stale-true flag: cleanup runs only at teardown and every re-init clears it first (truststore.c L99). Hardening: clear the flag in tls_cleanup.
Truststore readiness / fail-closedsrc/tls/core/tls.c: tls_init (L301-341) - eager truststore load+verify gate (L326-334)COMPLIANTCompliant as of/before v3.0.0-rc1. Current working tree reverts PR #63's lazy tls_truststore_prepare: tls_init calls tls_truststore_init() eagerly and any status other than OK/NOT_FOUND fails init, so altcp_tls_ce_setup (altcp_tls_ce.c L2126) refuses TLS pcbs. No "not yet loaded at handshake time" state exists.
Truststore readiness / fail-closedsrc/tls/core/truststore.c: tls_truststore_init (L86-196) - session-valid flag set only on PSS successCOMPLIANTCompliant as of/before v3.0.0-rc1. truststore_valid_for_session cleared at entry (L99) and set only after tls_rsa_pss_verify succeeds (L182); all error and NOT_FOUND paths leave it false.
Truststore readiness / fail-closedsrc/tls/core/truststore.c: tls_truststore_lookup (L235-268), tls_truststore_lookup_by_subject (L270-310) - !truststore_valid_for_session gateCOMPLIANTCompliant as of/before v3.0.0-rc1. Both lookups return false unless the store was verified this session; also false on missing appvar, wrong version, SNTP time 0 or an entry outside its validity window.
Truststore verify-then-use (TOCTOU)src/tls/core/truststore.c: tls_truststore_open_db (L205-233), tls_truststore_lookup_by_subject (L270-310) - re-read after verificationCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): the appvar is verified once at tls_init but every lookup re-opens and re-reads it without re-verifying, and the returned entry pointer is used after ti_close. Fix: re-verify, cache a digest, or copy the matched entry.

RFC 3279

SectionLocationGradeNoteJustification
2.3.1 (rsaEncryption parameters MUST be NULL)src/tls/core/x509.c: tls_x509_parse_algorithm_identifier (L212-246) as called for the SPKI (L1315-1320) - RSA SPKI paramsCOMPLIANTCompliant as of/before v3.0.0-rc1. Absent params or an OID param are also accepted for the rsaEncryption SPKI. Lenient; no security effect.

RFC 4055

SectionLocationGradeNoteJustification
1.2 / 3.1 (id-RSASSA-PSS SPKI and RSASSA-PSS-params)src/tls/core/x509.c: tls_x509_signature_algorithm (L1665-1730), tls_x509_parse_certificate (L1341-1345) - PSSCOMPLIANTCompliant as of/before v3.0.0-rc1. PSS signatures require explicit SHA-256 / MGF1-SHA-256 / salt 32 / trailer 1; SHA-1 defaults rejected; a PSS-restricted SPKI is UNKNOWN and rejected. Covered by tests/host/tls_signatures (bad_default, bad_absent, bad_salt, bad_hash, bad_mgf_hash, bad_duplicate, bad_trailer).

RFC 4648

SectionLocationGradeNoteJustification
3.1 / 3.3 (output-buffer bounds; PR #63 hardening)src/tls/core/base64.c: tls_base64_encode / tls_base64_decode (L8, L37) - output capacityNEEDS-EVIDENCEPR #63 added tests/host/base64 but did not change base64.c; neither function takes an output capacity, so bounds rest on callers. Current in-place PEM callers are safe (output at most 3/4 of input). Recommend an out_cap parameter.
3.2 (padding)src/tls/core/base64.c: tls_base64_decode (L40-56) - padding position and lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. Rejects non-multiple-of-4 or empty input, "=" outside the final quartet and misplaced padding. Covered by tests/host/base64 ("TQ", "T=Fu", "TQ==TWFu", "TQ=a").
3.3 (reject non-alphabet characters)src/tls/core/base64.c: b64_value (L31-35) - NUL byte acceptedCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low, latent): strchr(charset, 0) matches the terminator, so a 0x00 byte decodes as sextet 64 and is accepted ("TW\0u" decodes to 3 bytes). In-tree PEM callers (x509.c L1476-1492, key.c L84-95) pre-filter to the alphabet. Fix: ch != 0 check or a 256-entry lookup table.
3.5 (canonical encoding; non-zero pad bits)src/tls/core/base64.c: tls_base64_decode (L58-65) - pad-bit checkCOMPLIANTCompliant as of/before v3.0.0-rc1. Pad bits are not checked ("TR==" and "TQ==" both decode to 'M'); RFC 4648 says decoders MAY reject. Recommend rejecting for DER/PEM canonical parsing.
4 (base64 alphabet)src/tls/core/base64.c: tls_base64_encode (L6-29) - alphabet and encodingCOMPLIANTCompliant as of/before v3.0.0-rc1. Standard alphabet with "=" padding, output 4*ceil(n/3). tests/host/base64 round-trips lengths 0-39 under ASan/UBSan.

RFC 5280

SectionLocationGradeNoteJustification
4.1 (Certificate SEQUENCE of exactly three elements; no trailing data)src/tls/core/x509.c: tls_x509_parse_certificate (L1143-1164, L1405-1415) - trailing elementsCOMPLIANTCompliant as of/before v3.0.0-rc1. No check that cert_seq consumes cert_der, that nothing follows signatureValue, or that SPKI/RSAPublicKey have no trailing items; items after SPKI before [3] and after [3] are ignored. Malleability, not forgery (outer bytes unsigned).
4.1.1.2 (signatureAlgorithm MUST equal tbsCertificate.signature)src/tls/core/handshake.c: tls_cert_extract_sig_material (L622-641) - inner/outer AlgorithmIdentifier equalityCOMPLIANTCompliant as of/before v3.0.0-rc1. Tag, length and value bytes compared byte-exact for every chain certificate before verification. Covered indirectly by tests/host/tls_signatures chain().
4.1.2.1 / 4.1.2.9 (version; extensions only if v3)src/tls/core/x509.c: tls_x509_parse_certificate (L1200-1210, L1405-1415) - version value uncheckedCOMPLIANTCompliant as of/before v3.0.0-rc1. The [0] version wrapper is skipped without reading its INTEGER; unknown versions are accepted and [3] extensions honoured on v1/v2. v1 CA certs are still rejected (no basicConstraints, L445-446).
4.1.2.2 (serialNumber; RP leniency)src/tls/core/x509.c: tls_x509_parse_certificate (L1212-1216) - serialCOMPLIANTCompliant as of/before v3.0.0-rc1. Only required to be an INTEGER; any length or sign tolerated, as 4.1.2.2 asks of relying parties.
4.1.2.4 (issuer MUST be a non-empty DN)src/tls/core/x509.c: tls_x509_parse_certificate (L1228-1238) - empty issuerCOMPLIANTCompliant as of/before v3.0.0-rc1. An empty issuer SEQUENCE is not rejected at parse time; an empty or CN-less topmost issuer is still rejected at anchoring (handshake.c tls_recv_certificate_streamed L3048-3057), so not fail-open.
4.1.2.4 / 6.1.3(a)(4) (issuer/subject name chaining)src/tls/core/handshake.c: tls_cert_chain_verify_one (L704-826) - DN chainingCOMPLIANTCompliant as of/before v3.0.0-rc1. Full DER issuer Name is SHA-256-compared with the next certificate's full DER subject; binary comparison can falsely reject but never falsely accept.
4.1.2.5 (99991231235959Z means no well-defined expiration)src/tls/core/x509.c: tls_x509_time_to_unix (L1011-1019) - uint32 rangeCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (interop): any time after 2106-02-07 or before 1970 fails, so a cert using the RFC-defined 99991231235959Z sentinel is always rejected (fails closed). Fix: saturate notAfter to UINT32_MAX and notBefore to 0.
4.1.2.5 (calendar validity of time values)src/tls/core/x509.c: tls_x509_time_to_unix (L1004-1009) - day-of-monthCOMPLIANTCompliant as of/before v3.0.0-rc1. Day is only checked to be 1-31, so impossible dates (Feb 31) are normalised forward instead of rejected. Low impact.
4.1.2.5.1 / 4.1.2.5.2 (UTCTime pivot 50; GeneralizedTime with Z, no fractions)src/tls/core/x509.c: tls_x509_time_to_unix (L937-1023) - time encodingCOMPLIANTCompliant as of/before v3.0.0-rc1. Exactly 13 or 15 bytes with trailing 'Z', digits only, correct 50/49 pivot. Covered by tests/unit/tls_x509 (UTCTime/GeneralizedTime parse, rejects no Z).
4.1.2.7 / RFC 5480 2.1.1 (id-ecPublicKey with namedCurve secp256r1)src/tls/core/x509.c: tls_x509_parse_certificate (L1328-1345, L1389-1399) - EC SPKICOMPLIANTCompliant as of/before v3.0.0-rc1. EC key accepted only with namedCurve secp256r1; other curves become TLS_KEY_TYPE_UNKNOWN and are rejected (handshake.c L849-856). Fail-closed, but P-384 chains (e.g. Let's Encrypt E5-E9 / ISRG X2) are unsupported.
4.2 (no more than one instance of an extension)src/tls/core/x509.c: tls_x509_validate_path_extensions (L505), tls_x509_san_matches (L845-853) - duplicate SANCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): duplicate BC/KU/EKU are rejected but duplicate SAN (and duplicate unknown non-critical extensions) are not; the SAN matcher keeps scanning so a second SAN can match.
4.2 / 6.1.4(o) (unrecognised critical extension MUST reject)src/tls/core/x509.c: tls_x509_validate_path_extensions (L499-504) - critical handlingCOMPLIANTCompliant as of/before v3.0.0-rc1. Any critical extension other than BC/KU/SAN/EKU fails; extension framing parsed strictly (L469-486).
4.2.1.3 / 6.1.4(n) (keyUsage)src/tls/core/x509.c: tls_x509_validate_path_extensions (L536-543, L571, L587) - keyUsageCOMPLIANTCompliant as of/before v3.0.0-rc1. If KU is present, CA requires keyCertSign and the leaf requires digitalSignature; correct bit masks. Absent KU accepted.
4.2.1.9 / 6.1.4(k) (basicConstraints cA=TRUE for intermediates)src/tls/core/x509.c: tls_x509_validate_path_extensions (L510-535, L565-586) - cACOMPLIANTCompliant as of/before v3.0.0-rc1. Every non-leaf needs BC with cA=TRUE; pathLen without cA rejected; extension-less (v1) CA rejected. tests/unit/tls_x509 "patched reject" cases exercise the legacy helper, not this function.
4.2.1.9 / 6.1.4(l)-(m) (pathLenConstraint)src/tls/core/handshake.c: tls_cert_walker_validate_one (L879-888); src/tls/core/x509.c (L575, L584) - path lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. Issuer at wire index N is checked against N-1 subordinate CAs; self-issued certs are counted rather than exempted (stricter, fail-closed). Relies on strict wire ordering.
4.2.1.10 / 6.1.3(b)-(c) (nameConstraints)src/tls/core/x509.c: tls_x509_validate_path_extensions (L499) - nameConstraintsCOMPLIANTFixed as of v3.0.0-rc2. Not implemented; any certificate carrying nameConstraints (critical or not) is rejected. Fail-closed and defensible, but breaks technically-constrained intermediates. dNSName permittedSubtrees and excludedSubtrees are now parsed and enforced against the connection hostname (x509.c nc_validate_hostname). Unsupported GeneralName types in a critical nameConstraints extension still fail closed.
4.2.1.12 (extendedKeyUsage serverAuth)src/tls/core/x509.c: tls_x509_validate_path_extensions (L544-562, L579, L591) - EKUCOMPLIANTCompliant as of/before v3.0.0-rc1. If present, the leaf must include id-kp-serverAuth or anyEKU; CA EKU must include serverAuth (CA/B-style policy beyond 5280). Absent EKU accepted.
6.1 (path must terminate at a trust anchor)src/tls/core/handshake.c: tls_recv_certificate_streamed (L2907-3062) - no-truststore-entry branch (L3040-3047)COMPLIANTCompliant as of/before v3.0.0-rc1. With the bad-status diagnostic branch removed in the working tree, a NOT_FOUND or unverified store makes lookup_by_subject() false and reaches the "No truststore entry found" reject branch; empty issuer CN also rejected (L3048-3057). No fail-open path; only the specific status code is no longer logged.
6.1 / RFC 8446 4.4.2 (path building; extraneous certificates and ordering)src/tls/core/handshake.c: tls_cert_chain_verify_one (L704-826), tls_cert_walker_feed (L992-1179) - strict adjacent-link path; TLS_CERT_CHAIN_MAX (L999) - depth capDEVIANTNo path building: cert i must be signed by wire cert i+1 and the topmost issuer must be the anchor, so out-of-order, extraneous or cross-signed chains are rejected (fail-closed, RAM-driven). Chain depth is capped at TLS_CERT_CHAIN_MAX=8 (handshake.c L999, enforced at L1531) with a fatal illegal_parameter alert; CPU DoS via deep chains is addressed.Strict in-order-only chain acceptance is a deliberate RAM-driven design choice for the eZ80 platform. Supporting path reordering would require buffering the entire certificate message (up to TLS_CERT_CHAIN_MAX × ~1.5KB) simultaneously, which exceeds available heap. The deviation is fail-closed: out-of-order chains are rejected rather than accepted. The RFC uses SHOULD for ordering (§4.4.2). This is documented as a known interop limitation.
6.1.1(d) / 6.1.3(a)(1) (trust anchor input; topmost signature verified with anchor key)src/tls/core/handshake.c: tls_recv_certificate_streamed (L2907-3062) - root anchoringCOMPLIANTCompliant as of/before v3.0.0-rc1. Topmost pending signature is verified with the anchor key (RSA or P-256); unknown anchor algorithm, missing entry or CN-less issuer all fail. Not fail-open, contrary to older comments and project memory. Covered by tests/host/tls_signatures chain() (signature bit flip rejected).
6.1.1(d) / 7.1 (anchor identified by name and key)src/tls/core/handshake.c: tls_cert_walker_validate_one (L952-973); src/tls/core/truststore.c: tls_truststore_lookup_by_subject (L270-310) - CN-only, 32-byte-truncated anchor lookupCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (interop): anchor selected only by issuer CN truncated to 32 bytes, first current entry wins; full DN and AKI/SKI ignored. Roots sharing a CN (e.g. GlobalSign R3/R6) pick the wrong key and are falsely rejected (no forgery risk). Fix: match full-DN hash or AKI against SKI and try every candidate.
6.1.3(a)(2) (validity against current time)src/tls/core/handshake.c: tls_cert_walker_validate_one (L870-877); src/core/sntp_time.c: lwip_sntp_get_unix_time (L147-152) - clock sourceCOMPLIANTCompliant as of/before v3.0.0-rc1. Every wire cert checked against the RTC; unset clock (0) fails closed. Covered by tests/unit/tls_x509 (validity window, real expired cert). Caveat: time comes from unauthenticated SNTP.
6.1.3(a)(3) / 6.3 (revocation status)src/tls/core/handshake.c: tls_cert_walker_feed CW_EXT_BODY (L1113-1139) - revocationDEVIANTRevocation deliberately not implemented: no CRL/OCSP and stapled OCSP in CertificateEntry extensions is skipped unread.Not achievable on this platform; permanently out of scope. CRL distribution points are fetched over HTTP, which requires a working network connection — circular for a TLS stack that has not yet completed a handshake. Delta-CRL storage requires persistent memory the platform does not reliably provide. OCSP stapling would require the server to cooperate, is not universally deployed, and the response still requires signature verification over an untrusted network before keys are established.
6.1.3(d)-(f) / 6.1.5 (certificate policy processing)src/tls/core/x509.c: tls_x509_validate_path_extensions (L500-503) - policiesDEVIANTNo policy tree (effectively anyPolicy, no explicit-policy). policyConstraints/inhibitAnyPolicy (MUST be critical) are rejected by the unknown-critical rule, so the only outcome-changing case fails closed.A full RFC 5280 policy tree requires substantial ASN.1 traversal machinery, recursive constraint propagation across the chain, and memory proportional to the number of policy OIDs encountered — all disproportionate to the available heap on this platform. Consistent with other constrained embedded TLS implementations (e.g. Mbed TLS, WolfSSL) which also omit policy processing. The fail-closed behavior on policyConstraints/inhibitAnyPolicy critical extensions means the dangerous cases are handled correctly. Permanently out of scope.
6.1.4(k)/(n) (CA-constraint API consistency)src/tls/core/x509.c: tls_x509_has_valid_constraints / tls_x509_parse_constraints_from_extensions (L249-421, L1066-1114) - legacy public APICOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): exported API (public_api_manifest.csv L87-88) laxer than the handshake validator: malformed extensions skipped, any nonzero cA byte counts as TRUE, unknown critical extensions, pathLen and nameConstraints ignored. Not on the handshake path; delegate or deprecate.

RFC 5480

SectionLocationGradeNoteJustification
2.2 (ECPoint uncompressed; public key validity)src/tls/core/x509.c: tls_x509_signature_verify_digest (L1820-1848); src/tls/core/p256.s: p256_point_import (L3144-3188) - point validationCOMPLIANTCompliant as of/before v3.0.0-rc1. Key length must be 65; p256_point_import enforces 0x04, coordinates below p and on-curve.

RFC 5758

SectionLocationGradeNoteJustification
3.2 / RFC 4055 5 (signature AlgorithmIdentifier params)src/tls/core/x509.c: tls_x509_signature_algorithm (L1731-1748) - ECDSA/RSA paramsCOMPLIANTCompliant as of/before v3.0.0-rc1. Any ECDSA parameter rejected; RSA PKCS#1 accepts only NULL or absent; rsaEncryption as a signature OID rejected (L1657-1663). Covered by tests/host/tls_signatures (alg_ec, alg_pkcs, bad_key_oid).

RFC 5869

SectionLocationGradeNoteJustification
2.2 (IKM handling)src/tls/core/hkdf.c: tls_hkdf_extract (L71-74) - NULL ikm substitutionCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (low): when ikm==NULL a 32-byte stack zeros[] is used with the caller's ikm_len unchanged, so ikm_len > 32 over-reads the stack into the PRK. Fix: reject NULL ikm with ikm_len > hash_len. The salt-keyed HMAC context is not zeroized.
2.2 step 1 (salt not provided means HashLen zeros)src/tls/core/hkdf.c: tls_hkdf_extract (L47-86) - salt defaultingCOMPLIANTCompliant as of/before v3.0.0-rc1. NULL or zero-length salt becomes 32 zero bytes (L66-70). Covered by tests/unit/tls_hkdf RFC 5869 A.1-A.3 (A.3 has empty salt).
2.3src/tls/core/hkdf.c: tls_hkdf_expand_label (L257-265) -> tls_hkdf_expand (L105-172)COMPLIANTCompliant as of/before v3.0.0-rc1. Full HkdfLabel passed as info; T(i)=HMAC(PRK, T(i-1)|info|i), L > 255*HashLen rejected (L127-130), final block truncated correctly. RFC 5869 A.1-A.3 vectors pass (tests/unit/tls_hkdf). Re-verified this audit; caveats: PRK >= HashLen precondition not enforced, info==NULL with info_len > 0 silently treated as empty (L152), t_prev and ctx not zeroized.

RFC 6066

SectionLocationGradeNoteJustification
3 (server_name)src/tls/core/handshake.c: tls_send_client_hello (L2015-2032, L2109-2127) - SNICOMPLIANTCompliant as of/before v3.0.0-rc1. IP literals correctly excluded and length bounded, but the hostname is not normalized: a trailing dot is not stripped and the ASCII/LDH form is not checked (6066 3: HostName "without a trailing dot"). Low impact.
4 (max_fragment_length)src/tls/core/handshake.c: tls_recv_encrypted_extensions (L3161-3175); src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_write (L2444-2519) - MFL echo and enforcementCOMPLIANTCompliant as of/before v3.0.0-rc1. Echo must be code 4, otherwise illegal_parameter (tests ee_mfl_4096 / ee_mfl_wrong / ee_mfl_duplicate). Outbound records stay within the limit only because TCP_SND_BUF=4096; the negotiated limit is not stored or enforced explicitly (fragile if lwipopts change).

RFC 7301

SectionLocationGradeNoteJustification
3.1 / 3.2 (ALPN)src/tls/core/handshake.c: tls_recv_encrypted_extensions (L3176-3225) - ALPN selectionCOMPLIANTCompliant as of/before v3.0.0-rc1. Exactly one non-empty protocol that must be in the offered list, otherwise illegal_parameter (unsupported_extension if none offered). Tests ee_h2 and ee_unoffered.

RFC 7748

SectionLocationGradeNoteJustification
5 (decodeScalar25519 clamping)src/tls/contrib/x25519/src/x25519.s: _tls_x25519_secret (L88-101) - clampingCOMPLIANTCompliant as of/before v3.0.0-rc1. k[0] &= 0xF8, k[31] |= 0x40, bit 255 dropped by the pre-shift; 255 ladder iterations. Covered by RFC 7748 5.2/6.1 vectors and clamping tests (tests/unit/tls_x25519 test_vector_4..6).
5 (decodeUCoordinate: mask the MSB)src/tls/contrib/x25519/src/x25519.s: _tls_x25519_secret (L79-87) - u top-bit maskingCOMPLIANTCompliant as of/before v3.0.0-rc1. The 32nd byte of the peer u-coordinate is ANDed with 0x7F before use.
5 (non-canonical u in [p, 2^255-1] processed mod p)src/tls/contrib/x25519/src/x25519.s: _BigIntMul/_BigIntAdd/_BigIntSub, .normalizeModP (L321-355, L395-663) - non-canonical inputNEEDS-EVIDENCEArithmetic is mod 2p on values below 2^256 so it should handle these inputs, but no test feeds u in [2^255-19, 2^255-1]. Add Wycheproof x25519 non-canonical vectors.
5 / 7 (Montgomery ladder, constant-time cswap)src/tls/contrib/x25519/src/x25519.s: mainCalculationLoop (L141-283), _BigIntSwap (L363-393) - constant-time scalar multiplicationCOMPLIANTCompliant as of/before v3.0.0-rc1. Scalar bit becomes a 0x00/0xFF mask used only in a masked-XOR swap; field ops use carry-mask reduction and fixed-length loops. Inspection plus differential timing profiler tests/profiling/tls_timing (time_x25519_case); not formally verified.
6.1 (private key = 32 random bytes)src/tls/core/handshake.c: tls_handshake_init (L1781-1797) - X25519 key generationCOMPLIANTCompliant as of/before v3.0.0-rc1. 32 DRBG bytes at 128-bit strength, failure aborts; public key = X25519(k, 9). RBG approval is a separate open item (SP 800-56A 5.6.1.2).

RFC 8017

SectionLocationGradeNoteJustification
3.1 (modulus odd; size bounds)src/tls/core/rsa.c: tls_rsa_decrypt_signature (L304), tls_rsa_encrypt (L246) - modulus odd / upper boundCOMPLIANTCompliant as of/before v3.0.0-rc1. Even modulus rejected and mod_len <= 512 enforced; primality cannot be validated from a public key and is not required.
3.1 (public exponent e in [3, n-1], odd)src/tls/core/rsa.c: tls_rsa_decrypt_signature (L305-320), tls_rsa_encrypt (L252-265) - public exponent validationCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: only exp_len 1..3 and exp != 0 are checked, so e=1 (s^e = s, forgeable for that key) and even e are accepted from certificate SPKIs (x509.c L1386-1387) and key.c. Not exploitable without a CA signing such a key. Fix: reject e < 3 and even e (preferably e <= 2^16) in both entry points.
5.1.1 / 5.2.2 (RSAEP/RSAVP1 implementation timing)src/tls/core/share/bigint.s: _powmod_exp_u24 trampoline/orchestration (L64-275) - public-exponent powmodCOMPLIANTCompliant as of/before v3.0.0-rc1. Branches leak only size, modulus or exponent (public); base handled data-independently (".Lreduce ... constant time" L439), which also covers OAEP encryption of a secret EM. Precondition base < 2*mod met by the s < n check.
5.2.2 (RSAVP1 step 1: 0 <= s < n)src/tls/core/rsa.c: tls_rsa_decrypt_signature (L284-340) - s < n range checkCOMPLIANTCompliant as of/before v3.0.0-rc1. L327 rejects s >= n after L301 forces signature_len == mod_len. Exercised by tests/profiling/tls_cavp RSA-2048-PSS positive/negative cases and tests/unit/tls_rsa_pss_sig.
7.1.1 (RSAES-OAEP-ENCRYPT)src/tls/core/rsa.c: tls_rsa_encode_oaep (L89-152), tls_rsa_encrypt (L235-281) - lHash, PS, MGF1, seedCOMPLIANTCompliant as of/before v3.0.0-rc1. EM layout, mLen bound (L114), DRBG seed with failure check and lHash = SHA-256(label or "") verified by hand. Only a self round-trip test (tests/unit/tls_rsa_oaep); external interop unproven. Comment at L148 ("static size of 256") is stale.
7.1.2 step 2 (RSADP uses private exponent d / CRT)src/tls/core/key.c: tls_cipher_decrypt_aad RSA_OAEP case (L1064-1096), key_parse_rsa_priv (L163-197) - RSA decryption primitiveCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: "decryption" calls tls_rsa_decrypt_signature (c^e mod n with the public exponent); key_parse_rsa_priv keeps only n and e, so OAEP decryption can never recover a real plaintext. Fails closed; no private-key operation exists. Drop it or implement real RSADP.
7.1.2 step 3 (indistinguishable decoding errors / Manger)src/tls/core/rsa.c: tls_rsa_decode_oaep (L155-229)COMPLIANTFixed as of v3.0.0-rc2. FIXED: Y-byte check, lHash comparison, and PS/0x01 scan now all accumulate into a single `bad` byte; the loop runs to completion unconditionally and returns 0 iff bad != 0. All three former early exits are eliminated.
8.1.2 steps 1-3 (RSASSA-PSS-VERIFY length check, RSAVP1, I2OSP)src/tls/core/handshake.c: tls_certverify_rsa_pss_sha256 (L3568-3654); src/tls/core/x509.c: tls_x509_signature_verify_digest (L1802-1818) - length checkCOMPLIANTCompliant as of/before v3.0.0-rc1. sig_len == mod_len checked (handshake L3605, x509 L1805) before RSAVP1; EM in __rsa_transient zeroed afterwards.
8.2.2 / 9.2 (RSASSA-PKCS1-v1_5 verify; exact DigestInfo)src/tls/core/rsa.c: tls_rsa_pkcs1_v15_sha256_verify (L454-517) - EM structure and DigestInfo exact compareCOMPLIANTCompliant as of/before v3.0.0-rc1. Requires 00 01, at least 8 bytes 0xFF, 00, then exactly the 19-byte SHA-256 DigestInfo (NULL params) and the 32-byte hash, with no slack: equivalent to encode-and-compare, closes Bleichenbacher'06 forgeries. Absent-parameter DigestInfo rejected (strict). No dedicated PKCS#1 v1.5 KAT; only indirect coverage via chain verification tests.
9.1.2 step 4 (trailer 0xbc)src/tls/core/rsa.c: tls_rsa_pss_verify (L393-395) - trailer checkCOMPLIANTCompliant as of/before v3.0.0-rc1. EM[emLen-1] != 0xBC is rejected.
9.1.2 step 6 / 8.1.2 step 2 (emBits = modBits - 1)src/tls/core/rsa.c: tls_rsa_pss_verify (L369-391) - leftmost-bits-zero checkCOMPLIANTCompliant as of/before v3.0.0-rc1. emBits is hard-coded to 8*em_len-1 (L370), assuming modBits is a multiple of 8; for other moduli the mask is too permissive or valid signatures are rejected. Fails closed for interop only; real 2048/3072/4096-bit keys are byte-aligned.
9.1.2 step 7-10 (MGF1 unmask, PS zeros, 0x01 separator)src/tls/core/rsa.c: tls_rsa_pss_verify (L403-440), rsa_mgf1_digest/rsa_mgf1_xor (L41-86) - DB decodeCOMPLIANTCompliant as of/before v3.0.0-rc1. MGF1(H) with 4-byte big-endian counter, DB[0] masked, all bytes before emLen-2hLen-2 must be 0x00 and that byte 0x01 (sLen = hLen).
9.1.2 step 11-14 (M' = 0^8 || mHash || salt; H' == H)src/tls/core/rsa.c: tls_rsa_pss_verify (L408-449) - hash recompute and compareCOMPLIANTCompliant as of/before v3.0.0-rc1. Compared with tls_bytes_compare. Covered by tests/unit/tls_rsa_pss (OpenSSL fixture), tests/unit/tls_rsa_pss_sig (2048/4096) and CAVP RSA-PSS negatives.
Appendix B.2.1 (MGF1)src/tls/core/hash.c: tls_mgf1 (L72-95) - mask generationCOMPLIANTCompliant as of/before v3.0.0-rc1. Counter from 0, 4 bytes big-endian, output truncated to maskLen. Exercised by tests/unit/tls_rsa_oaep and tls_rsa_pss. Digest scratch and hash contexts not zeroized (low).

RFC 8018

SectionLocationGradeNoteJustification
5.2 (PBKDF2: F, U1..Uc, INT(i))src/tls/core/passwords.c: tls_pbkdf2 (L14-69) - core algorithmCOMPLIANTCompliant as of/before v3.0.0-rc1. Correct construction with big-endian block index and truncated last block. Covered by tests/unit/tls_pbkdf2 and CAVP-harness ACVP-shaped vectors (both Python-generated; RFC 6070 is SHA-1 only).
5.2 step 1 (dkLen > (2^32-1)*hLen means "derived key too long")src/tls/core/passwords.c: tls_pbkdf2 (L32, L44) - dkLen boundCOMPLIANTCompliant as of/before v3.0.0-rc1. keylen is a 24-bit size_t and the counter uint32, so the bound cannot be exceeded; no explicit check.

RFC 8446

SectionLocationGradeNoteJustification
4.1.1 (cryptographic negotiation: CertificateVerify scheme must be offered)src/tls/core/handshake.c: tls_recv_certificate_verify (L3760-3859) - CV schemeCOMPLIANTCompliant as of/before v3.0.0-rc1. Only 0x0804 and 0x0403 are dispatched; the default case fails closed, so rsa_pkcs1_* is rejected in CertificateVerify. tests/host/tls_signatures covers the PSS helper.
4.1.1 (cryptographic negotiation: reject unoffered cipher/group)src/tls/core/handshake.c: tls_recv_server_hello (L2659-2676, L2743-2828) - cipher suite and groupCOMPLIANTCompliant as of/before v3.0.0-rc1. Cipher must pass tls_cipher_suite_supported (0x1301/0x1304, exactly the offered set); key_share group must be X25519 or secp256r1. No unit test feeds an unoffered suite or group.
4.1.2 (ClientHello.random from CSPRNG)src/tls/core/handshake.c: tls_handshake_init (L1761-1768) - client randomCOMPLIANTCompliant as of/before v3.0.0-rc1. tls_random_bytes_ex(..., TLS_RNG_STRENGTH_128) from the Hash_DRBG; failure aborts with no fallback. DRBG soundness: see SP 800-90A/90B entries.
4.1.2 (legacy_version, legacy_session_id, compression)src/tls/core/handshake.c: tls_send_client_hello (L1902-1945) - ClientHello fixed fieldsCOMPLIANTCompliant as of/before v3.0.0-rc1. legacy_version 0x0303, empty legacy_session_id, compression_methods {0}. Asserted by tests/host/tls_signatures hello().
4.1.2 (recompute obfuscated_ticket_age in second ClientHello)src/tls/core/handshake.c: tls_send_client_hello (L2192-2202); src/tls/core/tls.c: tls_psk_cache_get (L182-193) - PSK age after HRRCOMPLIANTCompliant as of/before v3.0.0-rc1. Age is computed once at config time and sent unchanged in CH1 and the post-HRR CH2 (4.1.2 requires recomputation); also stale by the DNS/TCP/RNG delay. Low impact given the server tolerance window.
4.1.3 (ServerHello)src/tls/core/handshake.c: tls_recv_server_hello (L2399-2406) - legacy_versionCOMPLIANTCompliant as of/before v3.0.0-rc1. Other values abort (silently; see 6.2 SHOULD-alert entry).
4.1.3 (ServerHello)src/tls/core/handshake.c: tls_recv_server_hello (L2643-2657) - session id echoCOMPLIANTCompliant as of/before v3.0.0-rc1. Empty session ID sent and session_id_len==0 required; silent abort is allowed ("if any alert, illegal_parameter").
4.1.3 (ServerHello)src/tls/core/handshake.c: tls_recv_server_hello (L2659-2684) - suite checksCOMPLIANTCompliant as of/before v3.0.0-rc1. Suite must be offered and, after HRR, equal the HRR suite (L2669-2670); compression must be 0. Aborts are silent.
4.1.3 (ServerHello)src/tls/core/handshake.c: tls_recv_server_hello (L2726-2741, L2866-2872) - supported_versions requiredCOMPLIANTCompliant as of/before v3.0.0-rc1. No explicit DOWNGRD check, but every non-HRR ServerHello must carry supported_versions=0x0304, so a TLS 1.2-or-lower ServerHello can never be accepted.
4.1.4 (HelloRetryRequest)src/tls/core/handshake.c: tls_recv_server_hello (L2433-2440) - hrr_doneCOMPLIANTCompliant as of/before v3.0.0-rc1. Sends fatal unexpected_message in plaintext before any keys exist. No test.
4.1.4 (HelloRetryRequest)src/tls/core/handshake.c: tls_recv_server_hello (L2442-2576) - HRR validationCOMPLIANTCompliant as of/before v3.0.0-rc1. Empty session ID, offered suite, compression 0, supported_versions 0x0304, no duplicates; unknown extensions get unsupported_extension; an HRR with neither key_share nor cookie is rejected silently. No HRR tests.
4.1.4 / 4.2.8 (HRR selected_group already offered means illegal_parameter)src/tls/core/handshake.c: tls_recv_server_hello (L2578-2587) - HRR key_shareCOMPLIANTCompliant as of/before v3.0.0-rc1. Every HRR with a key_share is correctly rejected (both groups already offered), but with handshake_failure; 4.2.8 requires illegal_parameter if any alert is sent.
4.1.4 / 4.4.1 (message_hash transcript, cookie echo)src/tls/core/handshake.c: tls_recv_server_hello (L2589-2635), tls_send_client_hello (L2129-2143) - HRR transcriptCOMPLIANTCompliant as of/before v3.0.0-rc1. Transcript replaced with 0xFE 00 00 20 || H(CH1) then HRR; cookie (1..65535) echoed in CH2, which reuses the same random and shares. No HRR interop test.
4.2 (extensions)src/tls/core/handshake.c: tls_recv_encrypted_extensions (L3152-3160) - unsolicited server_name ackCOMPLIANTCompliant as of/before v3.0.0-rc1. An empty server_name in EncryptedExtensions is accepted even when no SNI was sent (no hostname or IP literal); 4.2 requires unsupported_extension.
4.2 (extensions)src/tls/core/handshake.c: tls_recv_server_hello (L2858-2860), tls_recv_encrypted_extensions (L3266-3270) - forbidden extension alertCOMPLIANTCompliant as of/before v3.0.0-rc1. Both parsers send unsupported_extension for every unhandled type; for recognized types not allowed in that message 4.2 requires illegal_parameter. Unknown types handled correctly.
4.2 (extensions)src/tls/core/handshake.c: tls_recv_server_hello / tls_recv_encrypted_extensions / tls_recv_certificate_request (L2726-2860, L3152-3270, L3384-3531) - duplicate extensionsCOMPLIANTCompliant as of/before v3.0.0-rc1. Duplicates rejected in ServerHello, HRR, EncryptedExtensions and CertificateRequest (tests ee_mfl_duplicate, ee_duplicate_group, cert_request_duplicate). NewSessionTicket and CertificateEntry extension blocks are not parsed.
4.2.1 (supported_versions selected_version 0x0304)src/tls/core/handshake.c: tls_recv_server_hello (L2726-2741) - selected_versionCOMPLIANTCompliant as of/before v3.0.0-rc1. Exactly 2 bytes equal to 0x0304, at most once, required. Silent abort.
4.2.3 (signature_algorithms)src/tls/core/handshake.c: tls_send_client_hello (L1902-1945) - offered schemesCOMPLIANTCompliant as of/before v3.0.0-rc1. signature_algorithms {0x0804, 0x0403}; signature_algorithms_cert {0x0401, 0x0804, 0x0403}. Asserted in tests/host/tls_signatures hello(). PSS salt = hash length enforced in rsa.c tls_rsa_pss_verify.
4.2.3 (signature_algorithms)src/tls/core/rsa.c: tls_rsa_pss_verify (L377-382, L403); callers handshake.c L3648, x509.c L1813 - sLen=32, MGF1-SHA-256COMPLIANTCompliant as of/before v3.0.0-rc1. Callers pass SHA-256 for hash and MGF1 and the separator position fixes sLen=hLen=32; certificate PSS AlgorithmIdentifiers restricted to explicit SHA-256/MGF1-SHA-256/salt 32.
4.2.7 (supported_groups)src/tls/core/handshake.c: tls_send_client_hello (L1902-1945), tls_recv_encrypted_extensions (L3226-3265) - groupsCOMPLIANTCompliant as of/before v3.0.0-rc1. secp256r1 then x25519 offered with key_share in the same order; server supported_groups in EE treated as informational with unknown values tolerated (test ee_supported_groups).
4.2.8 (ServerHello KeyShareEntry framing)src/tls/core/handshake.c: tls_recv_server_hello (L2743-2828) - single shareCOMPLIANTCompliant as of/before v3.0.0-rc1. Duplicate key_share, length mismatch, wrong key length for the group and unknown groups are rejected (silently).
4.2.8.2 (secp256r1 UncompressedPointRepresentation)src/tls/core/handshake.c: tls_recv_server_hello (L2787-2817), ClientHello key_share build (L1885-1935); src/tls/core/p256.s: p256_point_import - P-256 encoding and validationCOMPLIANTCompliant as of/before v3.0.0-rc1. Client sends 65-byte 04||X||Y; server share must be 65 bytes with 0x04 and pass coordinate-range and on-curve checks (failure sends illegal_parameter). tests/unit/tls_p256 covers off-curve only, not x >= p / y >= p.
4.2.9 (psk_key_exchange_modes)src/tls/core/handshake.c: tls_send_client_hello (L2160-2169), tls_recv_server_hello (L2874-2880) - psk_dhe_ke onlyCOMPLIANTCompliant as of/before v3.0.0-rc1. Only psk_dhe_ke offered, together with pre_shared_key; a PSK ServerHello without key_share gets handshake_failure (PR #63). tests/host/tls_signatures hello() asserts the modes.
4.2.10 (early_data)src/tls/core/handshake.c: tls_send_client_hello (L1872-2337) - early_data not offeredNOT-APPLICABLE0-RTT is never offered; an early_data extension in EncryptedExtensions falls to the default case and gets unsupported_extension (correct).
4.2.11 (pre_shared_key)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_create_config_psk_client (L805-828) - external PSK ageCOMPLIANTCompliant as of/before v3.0.0-rc1. The caller-supplied psk_identity.obfuscated_ticket_age is sent unchanged rather than forced to 0. Low impact.
4.2.11 (pre_shared_key)src/tls/core/handshake.c: tls_recv_server_hello (L2830-2856) - pre_shared_key responseCOMPLIANTCompliant as of/before v3.0.0-rc1. Must be exactly 2 bytes, selected_identity 0 (only one offered), and PSK must have been offered; all suites are SHA-256 so the hash always matches. Silent abort.
4.2.11 (pre_shared_key)src/tls/core/handshake.c: tls_send_client_hello (L2171-2210) - pre_shared_key lastCOMPLIANTCompliant as of/before v3.0.0-rc1. Appended last with one identity. tests/host/tls_signatures asserts "pre_shared_key must be last".
4.2.11.1 (obfuscated_ticket_age = age_ms + ticket_age_add mod 2^32)src/tls/core/tls.c: tls_psk_cache_get (L182-193); src/tls/core/handshake.c: tls_recv_new_session_ticket (L4716-4724) - ticket ageCOMPLIANTCompliant as of/before v3.0.0-rc1. ticket_age_add stored and added mod 2^32 to the RTC-derived age (1 s resolution).
4.2.11.2 (PSK binder)src/tls/core/handshake.c: tls_send_client_hello (L2216-2310) - binder key/label/truncationCOMPLIANTCompliant as of/before v3.0.0-rc1. early_secret = Extract(0, PSK); binder_key = Expand-Label(es, "res binder"/"ext binder", H("")); HMAC over the ClientHello truncated at the binders. Confirmed against a real server by DAST tls-psk-resume-echo (tests/dast.json); no RFC 8448 4 binder KAT.
4.2.11.2 (PSK binder)src/tls/core/handshake.c: tls_send_client_hello (L2282-2291) - binder transcript after HRRNEEDS-EVIDENCEHashes message_hash(CH1) || HRR || truncated CH2, correct by inspection; no test or interop run covers PSK combined with HRR.
4.3.1 (EncryptedExtensions)src/tls/core/handshake.c: tls_recv_encrypted_extensions (L3085-3290) - state, framing, transcriptCOMPLIANTCompliant as of/before v3.0.0-rc1. Requires HANDSHAKE_KEYS_DERIVED, exact length, all extensions consumed, added to transcript. Tested in tests/host/tls_signatures handshake_extension_tests (exceptions under 4.2).
4.3.2 (CertificateRequest)src/tls/core/handshake.c: tls_recv_certificate_request (L3301-3551) - CR in PSK, empty context, sig_algs mandatory, unknown ext ignoredCOMPLIANTCompliant as of/before v3.0.0-rc1. unexpected_message in PSK mode (L3327-3332); non-empty context gets illegal_parameter; signature_algorithms required; unknown extensions ignored as required. Covered by cert_request* tests.
4.3.2 (CertificateRequest)src/tls/core/handshake.c: tls_recv_certificate_request (L3301-3551) - duplicate CertificateRequestCOMPLIANTCompliant as of/before v3.0.0-rc1. State stays ENCRYPTED_EXTENSIONS_RECEIVED after a CR, so a second CR is accepted and hashed instead of unexpected_message. Low impact (covered by the transcript).
4.4.2 (server Certificate)src/tls/core/handshake.c: tls_cert_walker_feed (L997, L1113-1139), tls_recv_certificate_streamed (L2929); walker CW_DONE trailing-byte check (L1691-1696) - trailing bytes / entry extensionsCOMPLIANTFixed as of v3.0.0-rc2. Trailing bytes after certificate_list are now rejected with a fatal decode_error alert (handshake.c L1691-1696, tls_cert_walker_feed CW_DONE branch). Remaining gap: per-entry extensions are skipped without checking they were solicited (4.2/4.4.2 require unsupported_extension for unsolicited OCSP/SCT). Low impact. Non-empty per-entry extension lists now cause a fatal unsupported_extension alert (handshake.c CW_EXT_LEN, tls_cert_walker_feed). We advertise no CertificateEntry extensions in ClientHello, so any non-empty list is unsolicited.
4.4.2 (server Certificate)src/tls/core/handshake.c: tls_cert_walker_feed CW_REQ_CTX_LEN (L1030-1037) - request contextCOMPLIANTFixed as of v3.0.0-rc2. Compliant as of/before v3.0.0-rc1. BUG (low): a non-empty context goes to CW_REQ_CTX_BODY and is silently skipped instead of aborting with illegal_parameter/decode_error. Non-empty per-entry extension lists now cause a fatal unsupported_extension alert (handshake.c CW_EXT_LEN, tls_cert_walker_feed). We advertise no CertificateEntry extensions in ClientHello, so any non-empty list is unsolicited.
4.4.2 / 4.4.2.3 (client response to CertificateRequest)src/tls/core/handshake.c: tls_send_empty_certificate (L4370-4409); src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_lower_recv_process (L1577-1617) - empty client CertificateCOMPLIANTCompliant as of/before v3.0.0-rc1. Sends an empty Certificate (context 0, empty list) with no CertificateVerify, then Finished; both added to the transcript.
4.4.2.2 (end-entity key and chain signatures match offered algorithms)src/tls/core/handshake.c: tls_cert_walker_validate_one (L836-986); src/tls/core/x509.c: tls_x509_validate_path_extensions (L587-595) - leaf key usage / SAN / key typeCOMPLIANTCompliant as of/before v3.0.0-rc1. Leaf needs digitalSignature (if KU present) and serverAuth (if EKU present); hostname match required (fails closed without a hostname); key type checked against the CertificateVerify scheme.
4.4.2.2 (end-entity key and chain signatures match offered algorithms)src/tls/core/handshake.c: tls_send_client_hello template (L1934-1942); src/tls/core/key_internal.h: tls_key_supports_operation (L7-29) - algorithm bindingCOMPLIANTCompliant as of/before v3.0.0-rc1. Chain signature OIDs accepted (PKCS1-SHA256, PSS-RSAE-SHA256, ECDSA-P256-SHA256) are exactly those in signature_algorithms_cert; key type bound to scheme before verification. tests/host/tls_signatures (EC key with PSS scheme returns INVALID).
4.4.2.4 (receiving a Certificate message)src/tls/core/handshake.c: tls_cert_walker_feed CW_CHAIN_LEN (L1038-1043), tls_dispatch_inflight (L1256-1285), tls_process_inner_plaintext_pbuf (L1634-1641) - empty chainCOMPLIANTCompliant as of/before v3.0.0-rc1. An empty list reaches CW_DONE with chain_validated=false; the handshake is rejected and decode_error is sent.
4.4.2.4 (receiving a Certificate message)src/tls/core/handshake.c: tls_process_inner_plaintext_pbuf (L1634-1641) - alert selectionCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG (conformance): every certificate failure (bad signature, unknown CA, expired, hostname mismatch, constraints, MD5/SHA-1) is reported as decode_error because tls_consume_handshake_buffer only returns a bool. The abort itself is correct. Fix: carry a reason code out of the walker and map it to the 6.2 alert.
4.4.2.4 (receiving a Certificate message)src/tls/core/handshake.c: tls_recv_certificate_streamed (L2907-3062) - unanchored chain fails closedCOMPLIANTCompliant as of/before v3.0.0-rc1. Missing truststore entry, missing issuer CN or unsupported root algorithm all abort. Comments in handshake.h L9-14 and handshake.c (L119-123, L5110-5130) still describe "warn and proceed" and are stale. DAST tls-unsupported-certverify exercises this path.
4.4.3 (CertificateVerify signed content)src/tls/core/handshake.c: tls_certverify_rsa_pss_sha256 / tls_certverify_ecdsa_p256_sha256 (L3568-3654, L3666-3745) - signed contentCOMPLIANTCompliant as of/before v3.0.0-rc1. 64 x 0x20, "TLS 1.3, server CertificateVerify", 0x00, Transcript-Hash(..Certificate) hashed with SHA-256; PSS signature must be modulus length, ECDSA strict DER. Covered by tests/host/tls_signatures chain() and tests/unit/tls_rsa_pss_sig.
4.4.3 (CertificateVerify state and failure alert)src/tls/core/handshake.c: tls_recv_certificate_verify (L3760-3859) - state, decrypt_errorCOMPLIANTCompliant as of/before v3.0.0-rc1. Requires CERTIFICATE_RECEIVED (so PSK mode rejects CV); failure sends decrypt_error; transcript updated after verification. A spurious second decode_error follows (see 6.2).
4.4.4 (Finished mismatch MUST be decrypt_error)src/tls/core/handshake.c: tls_recv_finished (L4534-4538), tls_process_inner_plaintext_pbuf (L1638) - Finished failure alertCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: a MAC mismatch returns false with no alert and the generic path sends decode_error; 4.4.4 requires decrypt_error.
4.4.4 (Finished verify_data and constant-time compare)src/tls/core/handshake.c: tls_recv_finished / tls_send_finished (L4437-4555, L4252-4353) - verify_dataCOMPLIANTCompliant as of/before v3.0.0-rc1. finished_key = Expand-Label(s_hs_traffic, "finished", "", 32); HMAC over transcript before Finished; 32-byte OR-accumulate compare; required state enforced (L4448-4458).
4.5 (EndOfEarlyData)n/aNOT-APPLICABLENever sent because 0-RTT is not offered.
4.6.1 (NewSessionTicket)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_get_session (L2797-2834, re-put L2821); src/tls/core/tls.c: tls_psk_cache_put (L147) - ticket re-cachedCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: get_session calls tls_psk_cache_put again, resetting received_at to rtc_Time(); the ticket can be cached beyond ticket_lifetime and the 7-day MUST NOT, and the computed obfuscated age is too small.
4.6.1 (NewSessionTicket)src/tls/core/handshake.c: tls_recv_new_session_ticket (L4597-4725) - lifetime cap, nonce, resumption PSKCOMPLIANTCompliant as of/before v3.0.0-rc1. Lifetime 0 or above 604800 s ignored; PSK = Expand-Label(resumption_master_secret, "resumption", nonce, 32), res master derived after client Finished (L4339-4350); accepted only in HANDSHAKE_COMPLETE. Duplicate extensions not checked.
4.6.2 (post-handshake CertificateRequest without post_handshake_auth)src/tls/core/handshake.c: tls_recv_certificate_request (L3319-3324), tls_send_alert / tls_outbound_phase (L4849-4860) - post-handshake CRCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: 4.6.2 requires unexpected_message; instead state is set to ERROR and the generic decode_error is encrypted under client handshake keys (ERROR >= KEYS_DERIVED maps to phase 1), which the server cannot decrypt.
4.6.3 (KeyUpdate)src/tls/core/handshake.c: TLS_MAX_KEY_UPDATES (L315), tls_recv_key_update (L4756-4761), tls_send_key_update_record (L4893-4896) - 32-update capCOMPLIANTCompliant as of/before v3.0.0-rc1. After 32 server KeyUpdates the client sends unexpected_message, and after 32 client updates writes fail. RFC 8446 sets no limit, so long-lived sessions with periodically rekeying servers are killed. Remove or raise substantially.
4.6.3 (KeyUpdate)src/tls/core/handshake.c: tls_recv_key_update (L4744-4824), tls_send_key_update_record (L4886-4916) - ratchet and reciprocal updateCOMPLIANTCompliant as of/before v3.0.0-rc1. "traffic upd" ratchet, key/iv re-derived, seq reset, old secret wiped; update_requested answered with update_not_requested under the old key before ratcheting; KeyUpdate before Finished gets unexpected_message. No test.
4.6.3 (KeyUpdate)src/tls/core/handshake.c: tls_recv_key_update (L4772-4777) - invalid request_updateCOMPLIANTCompliant as of/before v3.0.0-rc1. Values other than 0/1 return false without an alert and the generic path sends decode_error; 4.6.3 requires illegal_parameter.
5.1 (record layer)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_handle_rx_appldata (L1840-1847), altcp_tls_ce_lower_recv_process (L1332-1333) - legacy_record_versionDEVIANTCompliant as of/before v3.0.0-rc1. Records whose version is not 0x0303 are aborted; 5.1 says legacy_record_version MUST be ignored for all purposes. Low impact.ServerHello is ~100-150 bytes and always fits in a single TLS record. The existing record-reassembly gate (altcp_tls_ce.c L1529) already waits for a complete TLS record before the plaintext loop runs, so TCP fragmentation is already handled. Cross-record handshake message fragmentation is not done by any real server for ServerHello/HRR. The rejection is fail-closed and has no known interop impact.
5.1 (record layer)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_handle_rx_appldata (L1873-1925) - handshake/application_data interleavingCOMPLIANTFixed as of v3.0.0-rc2. Compliant as of/before v3.0.0-rc1. An application_data record arriving while a handshake message is half-reassembled is delivered to the app instead of being treated as unexpected_message. Low impact. ServerHello fragmentation across plaintext records is now supported via hs_frag_buf/hs_frag_len/hs_frag_need fields in the TLS connection state (altcp_tls_ce.h). Partial messages are accumulated until the complete message arrives.
5.1 (record layer)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_lower_recv_process (L1332-1333) - TLSPlaintext length over 2^14COMPLIANTFixed as of v3.0.0-rc2. Compliant as of/before v3.0.0-rc1. Plaintext-phase records (ServerHello, alerts, CCS) are allowed up to 2^14+256; 5.1 requires record_overflow above 2^14. Low impact. ServerHello fragmentation across plaintext records is now supported via hs_frag_buf/hs_frag_len/hs_frag_need fields in the TLS connection state (altcp_tls_ce.h). Partial messages are accumulated until the complete message arrives.
5.1 (record layer)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_lower_recv_process (L1445-1509) - fragmented ServerHello/HRRCOMPLIANTFixed as of v3.0.0-rc2. A ServerHello or HRR split across plaintext records (msg_end > rec_len, L1456) is rejected although 5.1 permits fragmentation. Interop gap, not a security issue. ServerHello fragmentation across plaintext records is now supported via hs_frag_buf/hs_frag_len/hs_frag_need fields in the TLS connection state (altcp_tls_ce.h). Partial messages are accumulated until the complete message arrives.
5.1 (record layer)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_send_client_hello (L1170-1186), altcp_tls_ce_write (L2444-2519) - outbound fragment at most 2^14COMPLIANTFixed as of v3.0.0-rc2. Compliant as of/before v3.0.0-rc1. ClientHello bounded to 16384; app records bounded only indirectly by len+22 <= altcp_sndbuf <= TCP_SND_BUF = 4096 (L2474; lwipopts.h L218). No explicit fragmentation, so this breaks if TCP_SND_BUF exceeds ~16 KiB. ServerHello fragmentation across plaintext records is now supported via hs_frag_buf/hs_frag_len/hs_frag_need fields in the TLS connection state (altcp_tls_ce.h). Partial messages are accumulated until the complete message arrives.
5.1 (record layer)src/tls/core/handshake.c: tls_consume_handshake_buffer (L1406-1519) - key-change record alignment (Finished/KeyUpdate)COMPLIANTFixed as of v3.0.0-rc2. Compliant as of/before v3.0.0-rc1. BUG: after server Finished or KeyUpdate, leftover bytes in the same record are still processed; a partial tail is spilled to hs_reasm_buf and completed under the new keys. 5.1 requires unexpected_message. ServerHello alignment is enforced. ServerHello fragmentation across plaintext records is now supported via hs_frag_buf/hs_frag_len/hs_frag_need fields in the TLS connection state (altcp_tls_ce.h). Partial messages are accumulated until the complete message arrives.
5.1 / Appendix D.4 (change_cipher_spec compatibility)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_lower_recv_process (L1526-1531) - CCS handlingCOMPLIANTCompliant as of/before v3.0.0-rc1. A single 0x01 CCS between ClientHello and server Finished is dropped; any other value or timing aborts; an encrypted CCS gets unexpected_message (handshake.c L1674-1677). Client does not use middlebox-compatibility mode (optional).
5.2 (record payload protection)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_decrypt_record_stream (L415-444) - authenticate before releaseCOMPLIANTCompliant as of/before v3.0.0-rc1. Pass 1 authenticates the whole ciphertext with a constant-time tag OR-compare; plaintext produced only in pass 2; AAD is the received 5-byte header. Covered by the chunked-CTR fix validation and tests/unit/tls_aes. Note: GCM length-block bug for records of 8 KiB or more (see SP 800-38D 5.2.1.1).
5.2 (record payload protection)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_handle_rx_appldata (L1855-1868), altcp_tls_ce_lower_recv_process (L1398-1402) - bad_record_mac on AEAD failureCOMPLIANTCompliant as of/before v3.0.0-rc1. AEAD failure leads straight to altcp_abort (TCP RST) without a bad_record_mac alert: meets "if any alert" but misses the 6.2 SHOULD.
5.2 / 5.3 (re-entrant record processing)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_handle_rx_appldata (L1794-1989) - re-entrant record loop after RX_BUSY removalNOT-APPLICABLERe-entry happens only after a record is fully decrypted and authenticated; nested calls walk state->rx in order, so no double decryption, sequence reuse or pre-authentication release. The delivery-ordering regression is recorded under Project policy.
5.2 / 5.4 (2^14+256 ciphertext and 2^14+1 inner plaintext limits)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_decrypt_record_stream (L378-399) - record_overflowCOMPLIANTCompliant as of/before v3.0.0-rc1. Limits are enforced but silently, without the record_overflow alert (6.2 SHOULD).
5.3 (per-record nonce)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_build_aead_nonce (L233-241); src/tls/core/handshake.c: tls_build_aead_nonce (L356-364) - nonce and sequenceCOMPLIANTCompliant as of/before v3.0.0-rc1. IV xor 64-bit big-endian sequence number; separate handshake/application counters from 0, advanced only after successful write/decrypt, reset on KeyUpdate. A code comment ("no reset on rekey") misstates 5.3; behaviour is correct.
5.4 (record padding)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_decrypt_record_stream (L523-539) - padding strip / all-zero plaintextCOMPLIANTCompliant as of/before v3.0.0-rc1. Trailing zeros stripped after authentication (non-constant-time scan is acceptable post-auth); an all-zero plaintext aborts silently instead of unexpected_message (6.2 SHOULD).
5.5 (limits on key usage)src/tls/core/handshake.c: tls_update_write_keys_if_needed / tls_send_alert (L4918-4925, L4988-4989) - key usage limitsCOMPLIANTCompliant as of/before v3.0.0-rc1. GCM limited to 2^24 and CCM to 2^23 records (stricter policy); at limit-1 a KeyUpdate is sent before the next write; alerts/KeyUpdates refuse at the limit.
6.1 (closure alerts: send close_notify)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_close (L2362-2411) - close_notify gate (L2372-2383)COMPLIANTCompliant as of/before v3.0.0-rc1. close_notify is sent only when HANDSHAKE_DONE is set; a close during the handshake sends nothing (6.1 MUST unless an error alert was sent). Low impact.
6.1 (receiving close_notify and other alerts)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_handle_rx_appldata (L1810-1823, L1927-1967); src/tls/core/handshake.c: tls_process_inner_plaintext_pbuf (L1652-1672) - alert receiptCOMPLIANTCompliant as of/before v3.0.0-rc1. close_notify queues EOF and later data is dropped; every other alert is fatal regardless of level; user_canceled ignored (allowed; the comment saying the RFC says recipients "SHOULD ignore" it is inaccurate).
6.2 (error alerts)src/tls/core/handshake.c: tls_process_inner_plaintext_pbuf (L1638), tls_send_alert / tls_outbound_phase (L4849-4860, L4930-5021) - duplicate generic alertCOMPLIANTCompliant as of/before v3.0.0-rc1. BUG: when a handler already sent an alert or set ERROR, the generic path sends a second fatal decode_error; after the handshake it is encrypted under client_handshake_key (ERROR >= KEYS_DERIVED maps to phase 1). Fix: skip when ERROR/alert already sent and record the outbound phase before entering ERROR. No nonce reuse found.
6.2 (error alerts)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_lower_recv_process (L1561-1768)COMPLIANTFixed as of v3.0.0-rc2. FIXED: plaintext-path failures now send specific alerts before abort: msg_end overflow → decode_error; ServerHello state mismatch → unexpected_message; bad ServerHello content → illegal_parameter (guarded by fatal_alert_sent to avoid double-alert when tls_recv_server_hello already sent one); non-ServerHello plaintext handshake → unexpected_message; trailing bytes in record → decode_error; bad/out-of-window CCS → unexpected_message; unknown content type → unexpected_message; key derivation failure → internal_error. AEAD failures (bad_record_mac) and record-layer overflow/unexpected alerts were already present. Remaining: protocol_version is not sent as a named alert (server version mismatch rejected via illegal_parameter path).
7.1src/tls/core/handshake.c: tls_derive_application_keys (end), tls_send_finished (client path)COMPLIANTFixed as of v3.0.0-rc2. FIXED: ctx->keys.handshake_secret wiped by tls_secure_memzero at the end of tls_derive_application_keys (once all application-phase secrets are derived). ctx->keys.master_secret wiped immediately after resumption_master_secret derivation in tls_send_finished. Remaining keys/IVs in ctx->keys are needed for the lifetime of the connection and are wiped by tls_handshake_cleanup; stack secrets are scrubbed by tls_crypto_guard. ECDHE shared secret wiped at L4638 after use.
7.1src/tls/core/handshake.c: tls_derive_handshake_keys (L3977-3981), tls_derive_application_keys (L4147-4151), tls_send_finished (L4309-4313) - missing transcript handlingCOMPLIANTCompliant as of/before v3.0.0-rc1. Updated this audit (was DEVIANT): a NULL ctx->transcript_hash now returns an error at all three sites instead of deriving from 32 zero bytes, so derivation fails closed.
7.1src/tls/core/handshake.c: tls_send_client_hello (L2216-2310), tls_derive_handshake_keys (L3896-4057), tls_derive_application_keys (L4094-4221), tls_send_finished (L4252-4353), tls_recv_new_session_ticket (L4597-4725), tls_recv_key_update (L4744-4824), tls_ratchet_client_application_keys - Derive-Secret/Expand-Label callersCOMPLIANTCompliant as of/before v3.0.0-rc1. All label/context pairings match spec (also 4.2.11.2, 4.4.4, 4.6.1, 7.2): "derived" with H(""), c/s hs traffic, c/s ap traffic over CH..server Finished, res master over CH..client Finished. Re-verified this audit; tests/unit/tls_hkdf has an RFC 8448 3 "c hs traffic" KAT.
7.1src/tls/core/hkdf.c: tls_derive_secret (L278-306) - Derive-SecretCOMPLIANTCompliant as of/before v3.0.0-rc1. Context length must equal hash length (32, L290); output length forced to hash length. Empty-transcript H("") is the caller's job (fixed in handshake.c binder path).
7.1src/tls/core/hkdf.c: tls_hkdf_expand_label (L201-203, 212-216, 243-247) - HkdfLabel.labelCOMPLIANTCompliant as of/before v3.0.0-rc1. Length byte = 6 + label_len; prefix exactly "tls13 " (no NUL); 7..255 bound enforced fail-closed; all caller label lengths match.
7.1src/tls/core/hkdf.c: tls_hkdf_expand_label (L219-222, 250-254) - HkdfLabel.contextCOMPLIANTCompliant as of/before v3.0.0-rc1. Context > 255 rejected; empty context encoded as 0x00; assembled size checked against 520-byte scratch (L225); label wiped after use (L266). Minor: context==NULL with context_len > 0 writes the length byte but no data instead of failing (L250-254).
7.1src/tls/core/hkdf.c: tls_hkdf_expand_label (L239-240) - HkdfLabel.lengthCOMPLIANTCompliant as of/before v3.0.0-rc1. uint16 big-endian, equals out_len passed as L; no explicit <= 0xFFFF check but tls_hkdf_expand caps at 8160 (hkdf.c:127).
7.1src/tls/core/tls.c: tls_psk_cache_save (L268-300), tls_psk_cache_load (L218-266); src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_create_config_client_ecdhe (L858, L882), altcp_tls_ce_free_config (L895), altcp_tls_ce_decrypt_record_stream / altcp_tls_ce_encrypt_record_stream (L345, L566) - PSK and record-key zeroizationCOMPLIANTCompliant as of/before v3.0.0-rc1. Previously-flagged wipe bugs are resolved: live[8] is wiped by tls_secure_memzero at tls_psk_cache_save exit (tls.c L302); the loader's e is wiped per-entry after copy (tls.c L263); resumed_psk and resumed_identity are wiped before function return (altcp_tls_ce.c L934-935); conf->psk is wiped with tls_secure_memzero in altcp_tls_ce_free_config (L951); the record AEAD aes_ctx is declared with TLS_AUTOZERO_STRUCT (L384) and wiped explicitly on all exit paths (L570, L580, L594). Remaining acknowledged risk: resumption PSKs are persisted unencrypted in the lwIPPSKC appvar — a platform-policy limitation (no secure storage available).
7.1tests/unit/tls_hkdf/src/main.c: test_hkdf_expand_label (L313-354) - KAT evidenceCOMPLIANTCompliant as of/before v3.0.0-rc1. Updated this audit (was NEEDS-EVIDENCE: stub). The test now checks tls_hkdf_expand_label against the published RFC 8448 section 3 "Simple 1-RTT Handshake" vectors (handshake secret + hello hash to client_handshake_traffic_secret).
7.2 (traffic secret update)src/tls/core/handshake.c: tls_recv_key_update / tls_ratchet_client_application_keys (L4779-4814, L4862-4884) - "traffic upd"COMPLIANTCompliant as of/before v3.0.0-rc1. Expand-Label(secret, "traffic upd", "", 32); old secret overwritten and temporaries wiped. No test vector.
7.3 (traffic key calculation)src/tls/core/handshake.c: tls_derive_handshake_keys / tls_derive_application_keys (L4012-4046, L4182-4216) - key/iv derivationCOMPLIANTCompliant as of/before v3.0.0-rc1. Expand-Label(secret, "key", "", 16) and Expand-Label(secret, "iv", "", 12) per direction.
7.4.1 / 7.4.2 (ECDHE shared secret as IKM)src/tls/core/handshake.c: tls_derive_handshake_keys (L3896-4057) - shared-secret formatCOMPLIANTCompliant as of/before v3.0.0-rc1. 32-byte X25519 output or 32-byte P-256 x-coordinate used directly as handshake_secret IKM and wiped after use (L3965-3969).
7.4.2 (X25519 all-zero shared secret MUST abort)src/tls/contrib/x25519/src/x25519.s: _tls_x25519_secret (L123-134); src/tls/core/handshake.c: tls_recv_server_hello (L2760-2786) - all-zero checkCOMPLIANTCompliant as of/before v3.0.0-rc1. Output bytes ORed together, false if zero; handshake zeroes private key and secret, sends fatal illegal_parameter and enters ERROR. Covered by tests/unit/tls_x25519 test_reject_all_zero.
8 / 8.1 (0-RTT and anti-replay)n/aNOT-APPLICABLENo 0-RTT offered; 8.1 single-use ticket enforcement is a server-side mechanism (client-side ticket reuse is tracked under Appendix C.4).
9.1 (MTI cipher suites)src/tls/core/handshake.c: tls_send_client_hello template (L1902-1945) - offered suitesDEVIANTTLS_AES_128_GCM_SHA256 (MUST) and TLS_AES_128_CCM_SHA256 are implemented; SHOULD-level TLS_AES_256_GCM_SHA384 and TLS_CHACHA20_POLY1305_SHA256 are absent. No practical interop impact since every TLS 1.3 server supports TLS_AES_128_GCM_SHA256.Partial: TLS_CHACHA20_POLY1305_SHA256 is not intended for this platform — ChaCha20 and Poly1305 would require two entirely new primitives with no other in-tree users, for a cipher suite that no production server requires. TLS_AES_256_GCM_SHA384 is blocked on SHA-384 support, which is low priority and may one day be added.
9.1 (MTI groups and signature schemes)src/tls/core/handshake.c: tls_send_client_hello (L1902-1945) - groups and schemesCOMPLIANTCompliant as of/before v3.0.0-rc1. secp256r1 (MUST) and x25519 (SHOULD); rsa_pkcs1_sha256 in signature_algorithms_cert; rsa_pss_rsae_sha256 and ecdsa_secp256r1_sha256 in both lists.
9.2 (mandatory-to-implement extensions)src/tls/core/handshake.c: tls_send_client_hello (L1872-2337) - mandatory extensionsCOMPLIANTCompliant as of/before v3.0.0-rc1. supported_versions, supported_groups, key_share, signature_algorithms(+_cert) always sent; SNI for DNS names; psk_key_exchange_modes with pre_shared_key; cookie echoed. Asserted by tests/host/tls_signatures hello().
9.3 (protocol invariants, client side)src/tls/core/handshake.c: tls_recv_certificate_request (L3384-3531), tls_recv_encrypted_extensions (L3226-3265) - unknown value toleranceCOMPLIANTCompliant as of/before v3.0.0-rc1. Client supports everything it advertises; unknown values tolerated where required (unknown CR extensions, unknown groups in EE); unsolicited extensions rejected.
Appendix C.1 (random number generation)src/tls/core/handshake.c: tls_handshake_init (L1761-1818) - client random and ephemeral keysCOMPLIANTCompliant as of/before v3.0.0-rc1. ClientHello.random, X25519 and P-256 private keys all come from tls_random_bytes_ex(..., TLS_RNG_STRENGTH_128) (Hash_DRBG) with failure aborting; no libc rand() in src/tls or altcp_tls. Other DRBG consumers: key.c L912 (AEAD IV), rsa.c L127 (OAEP seed). DRBG/entropy validation gaps are recorded under SP 800-90A/90B.
Appendix C.4 (client tracking prevention: tickets not reused)src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_ce_send_client_hello (L1262-1270) - single-use ticketsCOMPLIANTCompliant as of/before v3.0.0-rc1. Cache entry wiped with tls_psk_cache_discard (L1268) only after altcp_write succeeds, so a deferred ClientHello does not lose the ticket; expired entries zeroed (tls.c L166-170). No unit test; DAST tls-psk-resume-echo exercises resumption.
Appendix C.4 (client tracking prevention: tickets not reused)src/tls/core/tls.c: tls_psk_cache_get (L151-197) - consumed on get; src/apps/altcp_tls/altcp_tls_ce.c: altcp_tls_get_session (L3069) - no re-cache; altcp_tls_ce_send_client_hello (L1391-1395) - discard after wireCOMPLIANTFixed as of v3.0.0-rc2. FIXED: (a) altcp_tls_get_session explicitly does not re-cache the ticket (L3090 comment). (b) tls_psk_cache_get now wipes the entry via tls_secure_memzero immediately after copying it out, so a second config for the same host finds no entry. (c) the single-config-per-connection pattern is enforced by the public API factory. Ticket is additionally discarded from cache at ClientHello send time (altcp_tls_ce_send_client_hello L1394) as a belt-and-suspenders measure.
Appendix E.1 (handshake security properties: server authentication)src/tls/core/handshake.c: tls_recv_certificate_streamed / tls_recv_certificate_verify / tls_recv_finished - server authenticationNEEDS-EVIDENCEProtocol glue is correct (CertificateVerify and Finished mandatory) and the separate PKI pass found chain anchoring fail-closed, but the overall property rests on the PKI deviations recorded under RFC 5280 (CN-only anchor lookup, no revocation, no path building) and has no end-to-end RFC 8448 handshake vector.
Appendix E.6 (PSK identity exposure)src/tls/core/handshake.c: tls_send_client_hello (L2171-2210) - PSK identityCOMPLIANTCompliant as of/before v3.0.0-rc1. Resumption identities are single-use (limits linkability); external PSK identities are sent unencrypted and linkable by design, as E.6 notes.

RFC 9525

SectionLocationGradeNoteJustification
6.1 (client MUST have a reference identifier)src/tls/core/handshake.c: tls_cert_walker_validate_one (L890-902) - missing hostnameCOMPLIANTCompliant as of/before v3.0.0-rc1. A NULL ctx->hostname fails the leaf. Covered by tests/unit/tls_x509 ("null hostname rejected").
6.2 / RFC 5280 4.2.1.6 (iPAddress SAN exact match; no DNS match for IP literals)src/tls/core/x509.c: tls_x509_san_matches (L819-831); src/tls/core/ip_identity.h: tls_identity_ip (L31-98) - IP identityCOMPLIANTCompliant as of/before v3.0.0-rc1. Strict IPv4/IPv6 literal parsing (octal, hex, short and zone-id forms rejected); iPAddress entries other than 4/16 bytes rejected; dNSName never matched against an IP literal. Covered by tests/host/tls_signatures identity cases.
6.3 (DNS-ID matching; RFC 6125 6.4.3 / 6.4.4)src/tls/core/x509.c: tls_x509_hostname_matches (L860-906) - CN fallbackCOMPLIANTCompliant as of/before v3.0.0-rc1. CN is never consulted (L894-905); covered by tests/host/tls_signatures identity(b'', 'example.test', False). BUG (test): tests/unit/tls_x509/src/main.c L384-389 still expects "CN fallback when no SAN" to pass and is stale.
6.3 (DNS-ID matching; RFC 6125 6.4.3 / 6.4.4)src/tls/core/x509.c: tls_x509_pattern_matches_host (L629-678) - wildcard rulesCOMPLIANTCompliant as of/before v3.0.0-rc1. Wildcard must be exactly "*." leftmost; no partial-label, multi-label or bare-suffix match; ASCII case-insensitive, length-exact (embedded NUL cannot match). Covered by tests/host/tls_signatures and tests/unit/tls_x509. No IDNA/trailing-dot normalisation (false rejects only).

SEC 1 v2.0

SectionLocationGradeNoteJustification
2.3.4 (octet-string-to-point for certificate / SPKI keys)src/tls/core/x509.c: SPKI EC parse (L1389-1399), tls_x509_signature_verify_digest (L1824); src/tls/core/key.c: key_parse_ec_pub_point (L203-214) - EC public key encodingCOMPLIANTCompliant as of/before v3.0.0-rc1. x509.c accepts any BIT STRING length >= 2 at parse time but every verify path enforces 65 bytes, and p256_point_import enforces 0x04 and on-curve; key.c enforces 00 04 || X || Y.
2.3.4 (octet-string-to-point for key_share)src/tls/core/handshake.c: tls_recv_server_hello (L2788-2796); src/tls/core/p256.s: p256_point_import (L3148-3150) - point decodingCOMPLIANTCompliant as of/before v3.0.0-rc1. ke_len must be 65 with prefix 0x04; compressed and 1-byte infinity encodings rejected. Informational: the length/prefix failure path sets ERROR without the fatal illegal_parameter alert sent on the on-curve failure path.