Input to auditor implementation (step 3). Rules are grouped in waves; wave 1 is the §10.8 minimal scanner from the project plan. Every rule yields findings with file:line, matched API, algorithm, confidence, and category.
PQC artifacts are 1–3 orders of magnitude larger than classical ones. Any code that assumes classical sizes breaks.
| Algorithm | Public key | Signature / ciphertext | Shared secret |
|---|---|---|---|
| RSA-2048 | ~294 B (SPKI) | 256 B sig | n/a |
| ECDSA P-256 | 65 B raw / ~91 B SPKI | 64 B raw / 70–72 B DER sig | n/a |
| Ed25519 | 32 B | 64 B sig | n/a |
| X25519 | 32 B | n/a | 32 B |
| ML-KEM-768 | 1,184 B | 1,088 B ct | 32 B |
| ML-KEM-512 / 1024 | 800 / 1,568 B | 768 / 1,568 B ct | 32 B |
| ML-DSA-65 | 1,952 B | 3,309 B sig | n/a |
| ML-DSA-44 / 87 | 1,312 / 2,592 B | 2,420 / 4,627 B sig | n/a |
| SLH-DSA (all sets) | 32–64 B | 7,856–49,856 B sig | n/a |
| Hybrid X25519MLKEM768 | 1,216 B keyshare | 1,120 B response | 32 B (combined) |
Sentinel constants for fragility detection: 32, 64, 65, 70, 72, 91, 128, 256, 294, 384, 512 appearing as buffer sizes / column widths / length checks near crypto dataflow.
javax.crypto.Cipher.getInstance(<literal>)where the algorithm token (before the first/) ∈ {RSA,ECIES}.java.security.KeyPairGenerator.getInstance(<literal>)where literal ∈ {RSA,DH,EC,DSA}.- Literal-only in wave 1; JCA names are case-insensitive, so match accordingly.
- Tests: positive/negative fixtures, transformation strings (
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"), casing variants, non-vulnerable algorithms ("AES","ML-KEM") as negatives.
| API | Vulnerable algorithm strings (aliases included) |
|---|---|
KeyPairGenerator / KeyFactory |
RSA, RSASSA-PSS, DSA, DH/DiffieHellman, EC, ECDSA, ECDH, X25519, X448, XDH, Ed25519, Ed448, EdDSA, ECMQV |
Signature |
SHA{1,224,256,384,512}with{RSA,ECDSA,DSA} (+andMGF1, inP1363Format variants), RSASSA-PSS, NONEwith{RSA,ECDSA,DSA}, Ed25519, Ed448, EdDSA |
KeyAgreement |
DH/DiffieHellman, ECDH, ECMQV, X25519, X448, XDH |
Cipher |
token before first / ∈ {RSA, ECIES} |
javax.crypto.KEM (JDK 21+) |
DHKEM (EC-based → vulnerable) |
AlgorithmParameters / spec classes |
RSAKeyGenParameterSpec, ECGenParameterSpec, DHParameterSpec, DSAParameterSpec, PSSParameterSpec, NamedParameterSpec.{X25519,X448,ED25519,ED448}, BC ECNamedCurveParameterSpec |
| TLS surface | setEnabledCipherSuites with TLS_ECDHE_*/TLS_RSA_* literals; jdk.tls.namedGroups values; SSLContext.getInstance("TLSv1.x") (pinning signal, see F3) |
JOSE/JWT (RFC 7518 alg) |
RS256/384/512, ES256/384/512, ES256K, PS256/384/512, EdDSA (jjwt, auth0 java-jwt, nimbus-jose), while HS* is fine |
| Concrete key interfaces | java.security.interfaces.{RSAPublicKey, RSAPrivateKey, RSAKey, ECPublicKey, ECPrivateKey, ECKey, DSAPublicKey, DSAPrivateKey, EdECPublicKey, EdECPrivateKey, XECPublicKey, XECPrivateKey}, javax.crypto.interfaces.{DHPublicKey, DHPrivateKey}, in casts, instanceof, fields, and method signatures |
| Informational only (not "vulnerable") | AES-128 key sizes, SHA-1, 3DES, MD5, reported at INFO severity for completeness |
| ID | Indicator | Detection sketch | Why it predicts effort |
|---|---|---|---|
| F1 | Fixed-size buffers near crypto dataflow | new byte[C], ByteBuffer.allocate(C), Arrays.copyOf(x, C) with C ∈ sentinel set, in same method as (or dataflow-adjacent to) a Key/Signature/Cipher value |
ML-DSA sig (3.3 KB) will not fit a 256 B buffer; every such site is a code change + retest |
| F2 | Fixed-width persistence / wire formats | JPA @Column(length=C) / columnDefinition="VARBINARY(C)" on key/sig/cert fields; DataOutputStream length-prefixed writes with constant lengths; fixed-offset parsing |
Schema migrations and wire-format changes are the most expensive class of change (coordination beyond one codebase) |
| F3 | Protocol/version/suite pinning | Literal arrays to setEnabledCipherSuites/setEnabledProtocols; hardcoded "TLSv1.2"; custom protocol magic/version constants adjacent to crypto |
Pinning forecloses negotiation paths hybrid migration depends on |
| F4 | Concrete-type coupling | Public API (params/returns/fields) typed as RSAPublicKey/ECPrivateKey/etc. instead of PublicKey/Key; casts/instanceof to them |
Type coupling propagates a change through every caller; interface-typed code migrates in place |
| F5 | Algorithm-name provenance (agility credit, inverse) | getInstance(arg): literal or static-final-resolved literal = rigid; sourced from config/properties/registry = agile |
Config-driven algorithm choice is exactly the crypto-agility CSWP 39 asks for; such sites migrate by config change |
| F6 | Persisted key material formats | KeyStore.getInstance("JKS"/"PKCS12") + cert chains; X509EncodedKeySpec/PKCS8EncodedKeySpec over stored bytes; hand-rolled PEM/DER parsing; Serializable classes with Key fields |
Stored material must be re-issued/re-encoded; data migration on top of code migration |
| F7 | Urgency class (separate axis) | Category of the call: key-establishment/encryption → HNDL-urgent; signature → CRQC-contingent | Orders the migration plan; kept out of the difficulty score to keep validation clean |
| F8 | Third-party API boundary | Vulnerable algorithm identifiers/keys crossing into external library calls we don't control (JWT builder alg enums, HSM/PKCS#11 mechanisms, cloud KMS SDK algorithm ids) | Migration is blocked on an upstream release: schedule risk, not just code effort |
- Detection is syntactic, by design (decided during implementation, deviating from the earlier "symbol resolution from wave 2" plan). Entry points are matched by the receiver's trailing type name. Rationale: JavaParser's symbol solver needs the target's full compiled classpath, which we will not have when scanning arbitrary case-study codebases we do not build. A purely syntactic matcher runs on any source tree and never aborts a scan.
- Constant propagation is implemented (
ConstantResolver+ConstantIndex), both intra-file (a local variable orfinalfield initialised to a literal) and cross-file (astatic final Stringreferenced asType.FIELD, resolved from a project-wide constant table the scanner builds in a first pass from the source itself, still with no compiled classpath). Confidence:HIGHfor a direct string literal,MEDIUMwhen any variable/field indirection was followed,MEDIUMfor type-coupling sites. This measurably matters on real code: across the four Phase-1 case studies it raised detected algorithm-selection call sites from 14 to 30 (seecase-studies/phase1-findings.md). - Still not resolved (the detection frontier, empirically identified in Phase 1): enum/registry-based algorithm selection (e.g. a library's
SignatureAlgorithm.RS256, which is library semantics, not constant propagation) and runtime-dynamic selection (getInstance(methodParam),getInstance(obj.getTransformation())). These need per-library modelling or dataflow, and are an accuracy trade-off to measure in the precision/recall evaluation (doc 05 §1), not a silent gap. - Source-level scanning confirmed as the right default (readable file:line for reports); ASM fallback remains deferred until a case study lacks source.
- Every rule carries a machine-readable ID (
JCA-KPG-RSA,JCA-SIG-ECDSA,FRAG-F4-RSAPublicKey, …). The report and the ground-truth labels join on these IDs (doc 05). - Implemented so far: wave 1 (Cipher, KeyPairGenerator); wave 2 (KeyFactory, KeyAgreement, Signature, plus F4 concrete key-type coupling); wave 3 fragility indicators F1 (fixed-size
byte[]/ByteBuffer.allocateat sentinel sizes), F3 (legacy TLS-version pins viaSSLContext.getInstance, andsetEnabledCipherSuites/setEnabledProtocolspinning, emitted asTLS_CONFIGfindings), and F6 (persisted key material viaKeyStore.getInstanceandX509/PKCS8EncodedKeySpec). Fragility indicators are attached to co-located crypto findings via an enclosing-scope merge (method/constructor, else type). Still pending: F2 (fixed-width persistence/wire formats), F8 (third-party API boundary), the JOSE/JWT surface, andLOW-confidence constant propagation / optional symbol resolution. The scoring engine already applies every multiplier as soon as a finding carries the indicator.