A ctlplne studio product

Weak keys

A key can be weak whatever its algorithm and size. In October 2026 eight eMudhra intermediate CAs turned out to have P-384 private keys that were small integers (4, 56, 66, 115, 155, 156, 230 and 237): anyone could compute them from the public key. trstctl checks every public key it rates for that kind of structural weakness, not only for its size.

The checks

The checks live in internal/crypto/weakkey and are modeled on the badkeys project (badkeys.info, MIT licence). They are re-derived from the published definitions; no badkeys code or data is copied.

Check What it finds
ec_small_scalar An elliptic-curve key whose private scalar k, or n − k, is at most 2^24 + 2^16, on P-224, P-256, P-384 and P-521. Baby-step giant-step over a table of the x-coordinates of 1·G … 2^16·G, built once per curve; about 4 ms per P-384 key. Comparing x-coordinates is what also catches n − k.
rsa_small_factor An RSA modulus divisible by a prime below 2^16.
rsa_close_primes Two RSA primes so close that Fermat's method factors the modulus within 100 rounds.
rsa_roca The ROCA structure (CVE-2017-15361, Infineon RSALib): the modulus lies in the subgroup generated by 65537 modulo each of the primes 3 … 167.
rsa_blocklist A key on an operator-supplied blocklist (below).
rsa_public_exponent An exponent no RSA key can have: below 3, or even.
rsa_shared_factor Two moduli in the same inventory that share a prime (Bernstein's batch GCD). Both can be factored. A key reused by several certificates is one modulus, not a shared factor. Batch GCD is a background routine, never run in a request path, bounded to 20,000 distinct moduli per run.
key_size, key_algorithm, signature_hash When a whole certificate is rated: RSA below 2048 bits, a curve below 256 bits, DSA, or a signature over MD2, MD5 or SHA-1.

Operator blocklists

trstctl ships no third-party key list. Debian's 2008 OpenSSL weak-key list (openssl-blacklist) is GPL-3, so the check reads files you supply, in that package's format: one entry per line, the last 20 hex digits of SHA-1("Modulus=" + uppercase hex modulus + "\n"), # comments allowed.

TRSTCTL_TRUST_WEAK_KEY_BLOCKLIST_FILES=/etc/trstctl/blacklist.RSA-1024,/etc/trstctl/blacklist.RSA-2048

The same setting is trust.weak_key_blocklist_files in the configuration file. A missing, empty or malformed file fails startup instead of checking nothing.

Where the checks run

  • Private keys an agent finds on a host. Every RSA size and curve is named (an RSA-1024 key used to be reported with no algorithm at all). A key below the floors, or with a structural weakness, is reported with key_strength: weak, the reason in key_weak_reason, and risk 95 instead of 85.

  • Rogue certificate detection (GET /api/v1/revocation/rogue-certificates, the console's Estate health tab). An inventory certificate whose bytes trstctl holds is rated from them: its key, its signature hash, and the distrust list by key, recorded chain and authority key identifier. A row that kept no bytes borrows the verdict its discovery reader recorded, or is rated from its recorded key-algorithm name and issuer name. A weak key is the weak_key_algorithm finding with its reasons in weak_key_reasons; a certificate that is, or chains to, a distrusted CA or key is the distrusted_issuer finding (risk 95, critical) with distrust naming the entry and how it matched: itself, chain, issuer_key_id, or issuer_name (the weakest: only the recorded issuer name matched). The recommendation is to replace it from a trusted issuer: claim it as a managed identity if it is not one and move it with a migration run. The old check read only the bare algorithm name, so every ECDSA certificate was flagged weak and no RSA-1024 certificate ever was.

  • Certificates an agent finds (files, Kubernetes secrets, PKCS#11 tokens, the Windows store, and OS, NSS, browser and Java trust stores). The agent rates each one while it holds the bytes, with the other certificates found in the same file as its chain, and reports the verdict as metadata (key_strength, key_weak_reason, distrust_entry, distrust_how, distrust_reason, distrust_subject) with the risk raised to 85 for a weak key and 95 for a distrusted certificate or issuer. A host trust store that holds emSign ECC Root CA - G3 is such a finding (distrust_how: itself).

  • The control plane re-rates every agent report against its own distrust list (shipped entries plus trust.distrust_file): by the reported key and certificate hashes, the authority key identifier and the issuer name. An agent's own distrust claim is replaced, never trusted; its weak-key reasons stand, since only it held the bytes. So an older agent, or an entry added after the agent rated a certificate, still produces the finding.

  • Cloud certificate discovery (AWS ACM, Azure Key Vault, Google Certificate Manager) rates each certificate from the bytes the provider returned, chain included where it comes back.

  • The CT monitor rates each hit it keeps (only those: a log batch can hold thousands of entries), from the logged certificate or precertificate; the rogue-certificate posture shows the verdict on the CT finding.

  • A relay's network scan observes the whole served chain: the leaf's key hash and authority key identifier, each served issuer's key and certificate hashes (up to eight), and the weak-key checks that fire on the leaf. The scan protocol admits no certificate bytes and no free text, so the relay sends those hashes and closed check codes only; the control plane matches them against its own distrust list and words the reasons.

  • The CBOM (POST /api/v1/cbom/scans, the console's Algorithms and future readiness screen). A TLS endpoint's certificate-key asset is rated from the served leaf and the served chain while the scan holds them. A key with a structural weakness, or a certificate that depends on a distrust-list entry, is weak and out of policy whatever its algorithm and size. Its reasons say why (weak key: …, or the entry and how it matched) and name the replacement path: plan its re-issue through the crypto migration flow (POST /api/v1/pqc/migrations/plan with the asset's id).

  • Every requester key trstctl certifies. A subject key that fails a structural check (small factor, close primes, ROCA, an operator blocklist hit, a small EC scalar, a bad RSA exponent) is refused before anything is signed, and the refusal names the check: ACME (badCSR, 400), EST and CMP (400, the check in the response), SCEP (the issuance evidence names it; the client sees a refusal), the REST identity transition with subject_csr_pem (400 before the transition is recorded), the Vault-compatible PKI engine (422), SPIFFE X.509-SVIDs, agent enrollment, CA authority issuance, and an external CA (422, before any connector is called). Size and algorithm floors are not part of this: they stay with each profile's key policy (allowed_key_algorithms, min_rsa_bits). An unset min_rsa_bits is 2048 bits; a profile that sets a lower floor explicitly still issues below it and raises a profile-rsa-floor-below-default finding until a later version raises it. Ed25519 and post-quantum keys have no structural checks and pass. The same paths refuse a key the tenant has recorded as compromised: one that a certificate of the tenant carried when it, or its identity, was revoked with reason keyCompromise (CA/B BR 6.1.1.3, condition 4). The key is recorded with the report, not with the publication: an identity's keyCompromise revocation records its certificates' SPKI SHA-256 in the same transaction, a revocation queued at an external CA records it when queued, and a direct revocation records it as it projects (backfilled at startup for earlier revocations). The check is one indexed lookup by tenant and hash, with no cap on how many revocations a tenant records. A bootstrap enrollment is checked for structural weakness before its single-use token is consumed; a key recorded as compromised is only known after the token names its tenant, so that refusal consumes the token.

  • The CA key ceremony. The signer refuses a weak key it generates, mints or imports; an HSM or KMS key that fails is zeroized and the operation fails with provider_key_weak. No weak key is certified as a CA, and an imported or cross-signed CA certificate that is weak or on the distrust list is refused. Before a signer-held key is certified or bound to an import, it signs a fresh challenge that must verify against the exact public key going into the certificate. An offline root, whose key never enters the signer, can sign its import ceremony's challenge on the offline system; without that signature the import records that possession was not proven. See the key ceremony runbook.

  • Removing a distrusted root from a host. A trust-store finding the distrust list names itself can be removed through trust distribution, by the agent that found it, when the host profile allows the path and the file holds only that root. See CA, trust-store & private-key discovery.

  • The inventory weak-key audit. Every six hours the leader runs batch GCD over each tenant's distinct RSA public keys: its CA authorities, the inventory certificates whose bytes trstctl holds, and the keys discovery readers report. Agents, cloud discovery and the CT monitor attach an RSA certificate's public key (rsa_spki, the base64 SubjectPublicKeyInfo); the control plane keeps it only when it hashes to the reported spki_sha256. Each run is a discovery run under the weak-key-audit:batch-gcd source, and every place a key that shares a prime appears gets a weak-key finding (risk

    1. that the rogue-certificate posture shows as a weak key, even for a certificate whose bytes trstctl holds. One run checks at most 20,000 distinct keys, CA authorities first, then inventory certificates, then reported keys; a larger inventory's run is partial and says how many keys it left out. Relay network scans report key hashes only, so their keys are not in the audit yet: that is what comes next for it.
  • Operator blocklists apply to what agents find. A reported RSA key is rated again on the control plane, which holds the operator's blocklists (trust.weak_key_blocklist_files); agents do not.

The distrust list

The shipped distrust list (internal/crypto/trust/distrust.json) names the three emSign roots and the eight weak intermediates by public key, and now by subject key identifier too. A certificate whose authority key identifier names one of them was issued under it, so a leaf under one of the eight intermediates is found even when no chain was recorded with it.

Rendered live from github.com/ctlplne/trstctl — found a mistake? edit this page.