Eight eMudhra intermediate CA certificates were backed by P-384 private keys that were not 384-bit secrets at all: each scalar was an integer below 256, and the largest was 237. The disclosure includes signatures over a fixed message that verify against all eight certificates, and walking multiples of the P-384 base point recovers the same scalars—{4, 56, 66, 115, 155, 156, 230, 237}—in at most 11 ms per key. eMudhra revoked its copies on October 18 and Sectigo its cross-signs on October 19, but the incident still matters for non-browser TLS clients whose trust stores move at package-manager speed.

A signature that covers all eight certificates

The site emudhra.we-have-a-problem.com published the material on October 20. It includes the eight certificates, their .pub.pem files, and, for each certificate, a signature over We have a problem here - Wayne. The signatures were made by the reporter who recovered the keys and sent them to root programs on October 17.

The claim can be checked without trusting that account. Extracting each certificate’s public key, comparing it with the published PEM, and verifying the signature with OpenSSL produces:


EM-DV-TLS-CA-G3B  pubkey MATCH   Verified OK
EM-DV-TLS-CA-G3C  pubkey MATCH   Verified OK
EM-EV-TLS-CA-G3B  pubkey MATCH   Verified OK
EM-EV-TLS-CA-G3C  pubkey MATCH   Verified OK
EM-OV-CS-CA-G3B   pubkey MATCH   Verified OK
EM-OV-CS-CA-G3C   pubkey MATCH   Verified OK
EM-OV-TLS-CA-G3B  pubkey MATCH   Verified OK
EM-OV-TLS-CA-G3C  pubkey MATCH   Verified OK

Every line matched. Control of all eight private keys is therefore demonstrated by arithmetic, not merely asserted by the publisher.

Recovering a P-384 key by counting

P-384 keys are meant to contain roughly 384 bits of entropy in the private scalar. For an elliptic-curve public point Q = d·G, a tiny d changes the problem completely: there is no need to solve a discrete logarithm when the scalar can simply be enumerated. The loop below is the complete search logic; only the point-addition helper is abbreviated in the published snippet.


def add(P, Q):  # point addition on P-384
    ...

R = None
for d in range(1, 512):
    R = add(R, (Gx, Gy))   # R = d*G, incrementally
    if R == (pubkey_x, pubkey_y):
        print("found", d)
        break

On a 2-vCPU Linux VM, starting at 1 and walking upward produced:


EM-OV-TLS-CA-G3C   d=4     0.000s
EM-EV-TLS-CA-G3C   d=56    0.002s
EM-OV-CS-CA-G3B    d=66    0.003s
EM-EV-TLS-CA-G3B   d=115   0.005s
EM-OV-TLS-CA-G3B   d=155   0.007s
EM-DV-TLS-CA-G3B   d=156   0.007s
EM-DV-TLS-CA-G3C   d=230   0.011s
EM-OV-CS-CA-G3C    d=237   0.011s

Those values agree with Mozilla’s incident record. The slowest search took eleven milliseconds, most of which was Python overhead. Once a scalar is known, an attacker can sign a certificate for any domain whose validation path reaches a root that trusts the intermediate.

Six of the eight intermediates are TLS CAs—DV, OV, and EV, across the G3B and G3C generations. The other two handle code signing. Counting Sectigo cross-signs, impacted-intermediates.json lists 16 entries, all revoked with reason ca_compromise.

How a second key pair reached production

In Mozilla incident bug 2083449, eMudhra describes a flaw in its key-ceremony script. During an approved deviation to test key wrap/unwrap, the script created a second key pair on the HSM. That pair, the weak one, carried labels similar to the real keys. The weak keys went into the CSRs, were signed by the emSign roots, and were sent to Sectigo for cross-signing. All eight certificates were created on April 30, 2025.

The warning came in May 2025, sixteen months before the public disclosure. CRLs for the six TLS intermediates began failing signature checks because they had been signed with the real keys, which did not match the certificates. Sectigo reported it. eMudhra re-signed the CRLs with the keys that did match the certificates—the weak ones—and closed the incident as a process error. The mismatch was the only control that fired, and it was explained away.

Browser blocks and the CT blind spot

The browser response was fast. Chrome pushed a CRLSet block, Firefox completed a OneCRL block on October 17, Apple devices blocked the certificates on October 19, and Mozilla removed the emSign ECC Root CA - G3 from NSS.

The disclosure also states that there are no Certificate Transparency log entries for these intermediates. A forged certificate would have had to be logged to work in Chrome or Safari, so CT-enforcing browsers could not have been attacked with certificates chained to them. Outside browsers, the situation changes: curl, Python, Go, and most non-browser TLS clients do not enforce CT. A certificate shown only to one of those clients need not leave a public log entry anywhere.

That leaves the long tail of trust stores that update through package managers on human timescales. Browser revocation does not reach those stores.

Testing a trust store

The disclosure site lists patched versions for multiple ecosystems, including Ubuntu ca-certificates 20260816, Python certifi 2026.10.19, and Go 1.27.3/1.26.10. It also supplies chains.zip, containing twelve chains: six through the emSign ECC root and six through AAA Certificate Services.

The offline test against a system store is a single command:


$ openssl verify -untrusted <chain>.pem <chain>.pem   # system store

On the test VM, six emSign chains still verified:


dv-g3b (emSign ECC root)   OK        <- still trusted
dv-g3c (emSign ECC root)   OK        <- still trusted
ov-g3b (emSign ECC root)   OK        <- still trusted
ov-g3c (emSign ECC root)   OK        <- still trusted
ev-g3b (emSign ECC root)   OK        <- still trusted
ev-g3c (emSign ECC root)   OK        <- still trusted
all six AAA chains         verification failed

Six of the twelve chains still validated. Live checks agreed: curl returned 200 from the non-AAA test hosts, and Node’s TLS stack accepted them too. The AAA hosts failed in both cases because the AAA root used by those chains had already been removed from the Mozilla store.

The installed packages explain the result. The machine had ca-certificates 20260601~24.04.1, not the patched 20260816ubuntu0.24.04.1, and certifi 2023.11.17, nearly three years behind 2026.10.19. It was a disposable development VM, but that is precisely the population at risk: systems nobody thinks of as running a trust store, all of which run one.

Two limits apply. The openssl verify run did not check revocation, following the site’s own offline method; clients that skip revocation are the ones exposed here. The curl test also went through a proxy with its own CA bundle, so the system-store verification was treated as the primary result.

Detection and mitigation

The command above is also the quickest detection check. An OK is not reassuring in this case: it means the weak chain is still trusted. Update the operating system trust store, certifi, and the Go toolchain to the fixed versions, then re-run the chain test.

Browser CRLSets, OneCRL, and Certificate Transparency do not protect a command-line or embedded client that never consults them. For software that validates TLS outside a browser, the package update cycle effectively becomes the revocation system.

The technical failure was small: an extra key pair, similar labels, and a selection error. The one control that noticed, a CRL signature mismatch in May 2025, was closed as a process mistake. The curve itself was not broken. For non-browser TLS clients, the update cadence is the real defense—and on the test machine, it was almost three years out of date.

References