TEE Remote Attestation Fallback Bypass
reported on 3 August 2026 via https://github.com/OpenNHP/opennhp/security/advisories/GHSA-v4q6-jq9w-f2xx
Summary
OpenNHP's server-side TEE (Trusted Execution Environment) remote
attestation check can be completely bypassed by any party who can submit a
DHP_KNK ("DHP knock") message to the server. The dispatcher that decides
whether to run AMD/Hygon CSV hardware attestation, or to unconditionally
succeed, is driven entirely by whether the attacker-supplied evidence JSON
contains a test_purpose key - there is no build tag, environment check, or
configuration flag gating this fallback path to non-production builds. In
addition, the fallback verifier's GetMeasure()/GetSerialNumber() methods
return each other's underlying fields (a field swap), which callers must
compensate for to land on a specific enrolled device entry, but which does
not otherwise change the impact.
The only thing standing between an attacker and a passing "Verified"
attestation result is knowledge of an already-enrolled (measure, serial_number) pair. Those values come from a plaintext TOML allowlist file
on the server (tee.toml) that ships with example values in the
project's own repository, and are not runtime secrets, cryptographic
material, or anything requiring proof of hardware possession to learn.
Details
nhp/core/verifier/verifier.go:
type Verifier interface {
Verify() error
GetSerialNumber() string
GetMeasure() string
}
type FallbackVerifier struct {
TestPurpose string `json:"test_purpose"`
Measure string `json:"measure"`
SerialNumber string `json:"serial_number"`
}
func (f *FallbackVerifier) Verify() error {
return nil
}
func (f *FallbackVerifier) GetSerialNumber() string {
return f.Measure
}
func (f *FallbackVerifier) GetMeasure() string {
return f.SerialNumber
}
func NewVerifier(compressedEvienceBase64 string) (Verifier, error) {
compressedEvidence, _ := base64.StdEncoding.DecodeString(compressedEvienceBase64)
r, _ := zlib.NewReader(bytes.NewReader(compressedEvidence))
evidenceBytes, _ := io.ReadAll(r)
var evidence map[string]any
json.Unmarshal(evidenceBytes, &evidence)
var verifier Verifier
if _, ok := evidence["test_purpose"]; ok {
verifier, err = NewFallbackVerifier(evidenceBytes)
} else {
verifier, err = csv.NewAttestation(string(evidenceBytes))
}
return verifier, err
}The choice between "run hardware attestation" (csv.NewAttestation,
which validates an actual AMD/Hygon confidential-computing report) and
"always succeed" (FallbackVerifier.Verify() unconditionally returns nil)
is made purely by inspecting attacker-controlled JSON for a test_purpose
key. There is no separate code path, build constraint, or config flag that
restricts FallbackVerifier to test/CI builds; it ships live in the same
binary an NHP server runs in production.
Confirmed by live testing that the getters are also swapped:
GetMeasure() returns the JSON serial_number field, and
GetSerialNumber() returns the JSON measure field.
The server-side call site, endpoints/server/config.go:
func (s *UdpServer) AppraiseEvidence(evidenceBase64 string) bool {
var measure string
var sn string
attestationVerifier, err := verifier.NewVerifier(evidenceBase64)
if err != nil {
return false
}
if err := attestationVerifier.Verify(); err != nil {
return false
}
measure = attestationVerifier.GetMeasure()
sn = attestationVerifier.GetSerialNumber()
s.teeMapMutex.Lock()
defer s.teeMapMutex.Unlock()
if _, exist := s.teeMap[measure]; exist {
s.teeMap[measure].Verified = true
return s.teeMap[measure].SerialNumber == sn
}
return false
}s.teeMap is populated at server startup by updateTee(), which reads a
plaintext TOML config file listing enrolled devices:
func (s *UdpServer) updateTee(file string) (err error) {
content, err := os.ReadFile(file)
var tees TeeAttestationReports
teeMap := make(map[string]*TeeAttestationReport)
toml.Unmarshal(content, &tees)
for _, tee := range tees.TEEs {
teeMap[tee.Measure] = tee
}
s.teeMap = teeMap
return err
}The project's own shipped example config
(endpoints/server/main/etc/tee.toml, also present under
docker/nhp-server/etc/tee.toml) shows the format:
[[TEEs]]
Measure = "19178a674248bbca705863bbf75ecaa049fcf3dfcc5ff59a80dcc5cbb60dae59"
SerialNumber = "TMEX300023050201"AppraiseEvidence() is called directly from endpoints/server/nhpauth.go
HandleKnockRequest, which processes every incoming DHP_KNK NHP packet
from a connecting agent:
if ppd.HeaderType == core.DHP_KNK { // dhp knock
if s.AppraiseEvidence(dhpKnkMsg.Evidence) {
dhpAckMsg.ErrCode = common.ErrSuccess.ErrorCode()
} else {
dhpAckMsg.ErrCode = common.ErrEvidenceAppraisalFailed.ErrorCode()
}
return
}A true return from AppraiseEvidence() is the sole gate between a
successful DHP-knock acknowledgement and a rejected one. Evidence in
common.DHPKnockMsg is a plain string field populated by whatever the
connecting client sends; nothing about it is bound to a proof of hardware
possession, only to the content of the JSON blob itself.
PoC
(available upon request)
Impact
Any party able to send a DHP_KNK NHP packet to an OpenNHP server, who also
knows one enrolled (measure, serial_number) pair from that server's
tee.toml, can obtain a passing attestation result (ErrSuccess ack,
teeMap[measure].Verified = true) without possessing the corresponding
confidential-computing hardware, without producing a AMD/Hygon CSV
attestation report, and without the csv.NewAttestation() verification
path ever executing. This completely defeats the purpose of the TEE
attestation feature: it can no longer be used to distinguish a genuine,
unmodified confidential-computing device from an attacker who merely knows
that device's (non-secret, config-file) identifier pair.
The (measure, serial_number) values are not cryptographic secrets or
proof of hardware possession. measure is a software/firmware measurement
hash (deterministic from known, often publicly documented firmware/kernel
images) and serial_number is a device identifier stored in a plaintext
allowlist file on the server; the project's own shipped example
configuration demonstrates the format with real-looking values checked
into the public repository. Any operational leak of this file, or reuse of
its example values, or disclosure of a device's serial number through any
other channel, is sufficient for an attacker to defeat attestation for that
device.
In this v1.0.0 release, the confirmed, code-verified consequence is the
attestation decision itself (the DHP_KNK ack outcome and the persisted
Verified flag). No other code path in this release was found to read
teeMap[x].Verified; the concrete "grant network access" logic used by
the standard NHP_KNK knock flow (handler.AuthWithNHP, which opens
ipset/eBPF firewall rules) is a separate code path not invoked by
DHP_KNK. This finding is reported as a broken authentication /
protection-mechanism failure of the attestation feature itself: the
security guarantee OpenNHP documents (a TEE-hardware-backed remote
attestation gate) is not actually enforced, and any policy or future
feature that trusts a Verified state, or a successful DHP_KNK ack, as
evidence of genuine confidential-computing hardware is misled.
version: 1.0.0
Source: OpenNHP/opennhp