Attacking RATS verifiers via the event log

Remote attestation is the process of a machine proving what it booted. The machine, the attester, collects evidence about its own state and sends it to a separate service, the verifier. The verifier checks that evidence and decides whether to trust the machine. Part of that evidence is the boot event log, a record of everything the machine measured while it booted. The machine’s TPM, a security chip (virtual, in a VM), vouches for that log by signing a running hash of those measurements.

Change one four-byte field in that log and the verifier’s integrity checks still pass, because nothing the TPM signs covers that field. But any verifier built on google/go-eventlog, Google’s library for parsing and replaying these logs, then crashes reading it.

Both published go-eventlog releases, v0.0.1 and v0.0.2, are affected, and so are go-tpm-tools v0.4.8 to v0.4.10, which pull go-eventlog in. On the go-eventlog releases and go-tpm-tools v0.4.8 and v0.4.9 the crash is a stack overflow. Go can’t recover from that, so the whole verifier process goes down, along with every request it was serving.

I found it with fuzzing, reported it to Google as issue 566744968, and sent the fix as #39.

how a verifier checks a boot log

When a machine boots, each stage hashes the next one before running it: the firmware hashes the bootloader, the bootloader hashes the kernel. Each hash is folded into a register in the TPM called a PCR. The TPM keeps only the result, so a PCR tells you something booted, not what.

The event log is the list of those steps. Each entry looks roughly like this:

PCR:    0
type:   EV_S_CRTM_VERSION
digest: <hash of what was measured>
data:   <a description of it>

To check the log, the verifier redoes the folding with each entry’s digest and compares the result to the TPM’s signed quote. If they match, the digests are genuine.

The type isn’t part of that check. It’s a label next to the digest, and nothing hashes it. Change it to anything and the log still verifies.

Checking the digests is only the first half of a verifier’s job. A digest is just a hash; it can’t tell you whether Secure Boot was on. The answer is in the event data, and the verifier has to find the right entry and decode it. The type is how it does both. To learn the Secure Boot state, it looks for entries in PCR 7 of type EV_EFI_VARIABLE_DRIVER_CONFIG and reads their data as UEFI variables.

So once the log passes, the verifier looks at every entry’s type, including the one the attacker wrote. That lookup is where go-eventlog crashes.

Ironically, Google already documents this in go-attestation, another of its libraries. Its event log disclosure says “the EventType isn’t part of the Digest and can be freely modified by an attacker.” Even go-eventlog’s own comment on Event.Type says unknown types “should result in errors.” It produces one, just not in a way any caller can handle.

a function that calls itself

This is the method go-eventlog uses to read an event’s type, tcg.Event.UntrustedType, as shipped in every release:

func (e Event) UntrustedType() EventType {
	tcgEvent := EventType(e.Type)
	if _, ok := tcgEvent.KnownName(); !ok {
		panic("library cannot convert between tpmeventlog EventType and tcg EventType for event " + e.UntrustedType().String())
	}
	return tcgEvent
}

If the type isn’t one the library has a name for, it panics. But to build the panic message it calls UntrustedType again, which hits the same unknown type and calls itself again, and so on. It never gets as far as panicking. Each call adds to the stack until it reaches Go’s 1 GB limit, and the runtime kills the process (runtime/debug.SetMaxStack).

reaching it from go-tpm-tools

Most verifiers get go-eventlog through go-tpm-tools. Its server.VerifyAttestation checks that the TPM’s attestation key (AK) chains to a trusted certificate authority, that the AK signed the quote, and that the replayed log matches the quote. The modified log passes all three. Then it parses the log to find out what booted, which ends up in UntrustedType:

tcg.Event.UntrustedType
extract.PlatformState
extract.FirmwareLogState
tpmeventlog.ReplayAndExtract
server.parsePCClientEventLog
server.parseMachineStateFromTPM
server.VerifyAttestation

To show it, I used a real signed attestation from a GCE VM that go-tpm-tools ships as a test fixture, internal/test/attestations/gce-cos-85-no-nonce.pb. In that log, the second entry’s type field starts at byte 77. Overwrite it with any type go-eventlog doesn’t know, and verify:

log := att.GetEventLog()
binary.LittleEndian.PutUint32(log[77:], 0x800000e1)

_, err := server.VerifyAttestation(att, server.VerifyOpts{
	TrustedRootCerts:  server.GceEKRoots,
	IntermediateCerts: server.GceEKIntermediates,
})

On go-tpm-tools v0.4.9:

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

one attestation away from a fleet outage

In remote attestation you don’t trust the attester until you’ve verified it. That’s the whole point. Every confidential VM running an attestation agent sends you its boot log before you’ve decided anything about it, so you have to assume any one of them could be hostile. The checks that run before the crash only need a TPM the verifier already accepts. If the verifier accepts Google’s TPM certificate authority, as the example above does, that is any GCE VM, including one the attacker creates in their own project.

Say that verifier is shared and hands out short-lived credentials on every successful attestation: a certificate, a token, a disk key. The attacker’s VM sends the modified log in a loop. Each copy of the verifier dies on it, restarts, and dies on the next one. Every other machine in the fleet stays healthy until its credential needs renewing, and then it can’t. Give it one credential lifetime and the whole fleet is locked out. Restarting the verifier doesn’t help, since the attacker is still sending. You have to ship the fix.

I haven’t tried this on a production fleet, but nothing in the design stops it.

which versions crash

  • go-eventlog: every tagged release (v0.0.1, v0.0.2). The fix, 1991f15, is on main and not yet in a release.
  • go-tpm-tools: v0.4.8 to v0.4.10 crash. v0.4.11 and later pull in the fix.

There is no CVE or Go vulnerability database entry, so govulncheck won’t flag it. To check your build, run go list -m github.com/google/go-eventlog. v0.0.1 and v0.0.2 are affected. So is any pseudo-version whose timestamp is before 20261001001437, the fix commit.

the fix, and what to do

Since #39, UntrustedType returns the type as it is, known or not. The code that reads the log skips entries whose type it doesn’t recognize, so the modified fixture now verifies without error. Which is what should happen, since the quote never covered the type.

What to do:

  1. Pin go-eventlog at or past the fix: go get github.com/google/go-eventlog@1991f15bc4b0.
  2. On go-tpm-tools, upgrade to v0.4.11 or later and confirm with go list -m.
  3. Fuzz the path from log bytes to extracted state. It’s the one input an attester fully controls.

disclosure timeline

  • 26 April 2024: go-eventlog’s initial commit includes the bug.
  • 27 September 2026: #39 opened. Merged 1 October as 1991f15.
  • 10 October 2026: go-tpm-tools v0.4.11 released with the fixed dependency.
  • 29 October 2026: this post.

what the fix doesn’t fix

The crash is fixed, but the type field is still unprotected. That’s how the log format works, and no library can change it.

A crash at least gets noticed. Worse is an attacker relabeling an entry the verifier cares about, so the verifier skips it and the log still passes. A recent issue does exactly that to hide Secure Boot revocations.

If you write a verifier, the quote proves the digests. Everything else in the log is the attester’s say-so.

Thanks to the Google Cloud Confidential Computing team for looking at this, merging the fix, and shipping it in a go-tpm-tools release.

If something here is wrong, tell me.