Finding 1/2: Are all the checkboxes ticked?
Missing Attestation Height Freshness Validation in Oasis Core's Rust SGX Path
Severity: Low
Affected component: SGXAttestation::verify() in Rust (runtime/src/consensus/registry.rs), missing parity with Go implementation (go/common/node/sgx.go)
Repository: github.com/oasisprotocol/oasis-core
Oasis Core maintains parallel Go and Rust implementations of SGX attestation verification. The Go side enforced a configurable freshness window, rejecting attestations older than roughly two hours by default. The Rust side omitted that height-based freshness check. As a result, the Rust verifier itself did not enforce the configured MaxAttestationAge bound, and could accept attestations older than that bound if no other layer rejected them first. The Rust module's own header stated it must be kept in sync with Go. It was not.
This issue was identified by Bluethroat Labs and responsibly disclosed to the Oasis team in January 2026.
Executive Summary
- The Go implementation of
SGXAttestationverification enforced a configurableMaxAttestationAgeheight check (default: 1200 blocks, roughly 2 hours). Attestations older than this window were rejected. - The Rust implementation of the same verification omitted this check entirely. The
verify()function signature accepted neithercurrent_heightnormax_attestation_age. - The
max_attestation_agefield existed in the RustSGXConstraintsstruct, but was dead code: never read, never enforced. - This meant the Rust verifier could permit a materially larger replay window than the configured ~2 hour default on paths that relied on it directly.
- The Rust module header explicitly stated: "This MUST be kept in sync with
go/registry/api."
Background
Attestation freshness is a defense against replay. When a TEE produces an attestation quote, that quote proves the platform's security posture at a specific point in time. If the quote can be replayed long after it was generated, the guarantee degrades: the platform's firmware may have been downgraded, a vulnerability may have been disclosed, or Intel may have revoked the platform's TCB status since the quote was produced.
Oasis Core addresses this by binding attestations to a consensus layer height and enforcing a maximum age. The Go implementation rejects attestations that are too far behind the current block height. This window is network-configurable via SGXConstraints.MaxAttestationAge, with a default of 1200 blocks (approximately 2 hours at Oasis's block time).
Vulnerability Details
The Go implementation enforces attestation freshness in verifyAttestationSignature:
// go/common/node/sgx.go, lines 278-284
// Check height is relatively recent and not from the future.
if sa.Height > height {
return ErrAttestationFromFuture
}
if age := height - sa.Height; age > sc.MaxAttestationAge {
return fmt.Errorf(
"node: TEE attestation not fresh enough (age: %d max: %d)",
age, sc.MaxAttestationAge,
)
}The function receives height (current consensus height) and sc (the SGXConstraints containing MaxAttestationAge). Both parameters are required for the freshness check to work.
The Rust implementation of the equivalent function omits both:
// runtime/src/consensus/registry.rs, lines 811-839
pub fn verify(
&self,
policy: &sgx::QuotePolicy,
node_id: &signature::PublicKey,
rak: &signature::PublicKey,
rek: &x25519::PublicKey,
) -> anyhow::Result<VerifiedAttestation> {
// Verify the quote.
let verified_quote = self.quote().verify(policy)?;
// Ensure that the report data includes the hash of the node's RAK.
Identity::verify_binding(&verified_quote, rak)?;
// Verify the attestation signature.
match self {
Self::V1 {
height, signature, ..
} => {
let h = Self::hash(&verified_quote.report_data, node_id, *height, rek);
signature.verify(rak, ATTESTATION_SIGNATURE_CONTEXT, &h)?;
Ok(VerifiedAttestation {
quote: verified_quote,
height: Some(*height),
})
}
_ => bail!("V0 attestation not supported"),
}
}The function verifies the quote cryptography, checks the RAK binding, and verifies the attestation signature. It does not check whether the attestation height is recent. The height value is extracted from the attestation and included in the returned VerifiedAttestation, but it is never compared against any maximum age.
The max_attestation_age field exists in the Rust SGXConstraints struct:
// runtime/src/consensus/registry.rs, lines 696-709
V1 {
#[cbor(optional)]
enclaves: Vec<sgx::EnclaveIdentity>,
#[cbor(optional)]
policy: sgx::QuotePolicy,
/// The maximum attestation age (in blocks).
#[cbor(optional)]
max_attestation_age: u64, // present in the struct, never read
},This field is present in the Rust structure but is never referenced in any verification path.
The Gap in Practice
Without the height freshness check, the Rust verification routine itself does not enforce the configured block-age limit. On any call path that relies on this verifier directly, acceptance is bounded by other checks, not by MaxAttestationAge.
The ROFL registration handler calls the Rust verification path:
rofl.Register → body.ect.verify(&cfg.policy.quotes) → no height checkAll ROFL registrations go through this path. The runtime-side verifier invoked there does not consult the configured MaxAttestationAge value. Whether some earlier layer also constrains stale attestations is a separate question from whether this verifier enforces the policy itself.
What Could Have Gone Wrong (But Did Not)
No exploitation of this vulnerability was observed during the assessment. The following scenarios describe what was possible given the code as written, not what occurred.
Stale platform re-registration. A ROFL instance could re-register using an attestation older than the configured freshness window if that attestation was still otherwise accepted by the verification stack. The Go path would have rejected the same attestation once it exceeded MaxAttestationAge.
Policy gap. Network operators could configure MaxAttestationAge expecting Rust-side verification to enforce it, but this verifier did not read that field at all. The policy existed in the Rust data structure but was not enforced here.
What This Did Not Allow
The missing freshness check did not allow forging attestation quotes. It did not bypass cryptographic verification. The attestation still had to be signed by a valid RAK, bound to a real enclave identity, and backed by valid TCB collateral. The vulnerability was confined to the time window: how long after generation a legitimate attestation could be replayed.
Why This Was Classified as Low
The Oasis team accepted the bug as a valid implementation gap but argued that practical exploitability was limited. Their position was that attestation freshness is also enforced at the consensus layer before execution reaches the runtime, meaning the Rust-side gap was defense-in-depth rather than a directly exploitable standalone boundary.
We originally submitted this as High, arguing that Rust-side verification paths such as ROFL registration and RPC session establishment were safer if they enforced freshness locally instead of relying on external layers. After discussion, the final classification was Low. The Oasis team committed to making the freshness constraint explicit in the Rust code.
We accepted the classification. The core observation stands: the Rust module header says "MUST be kept in sync with Go," and this check was not in sync. Whether the effect is defense-in-depth or directly security-relevant depends on the surrounding call path, but the safer design is to enforce the configured freshness bound in the verifier itself.
Fix
Add current_height and max_attestation_age parameters to the Rust verify() function and perform the same comparison the Go side already performs. The max_attestation_age field in SGXConstraints already exists and should be read instead of ignored. The fix is approximately two lines of logic, plus the function signature change.
Disclosure Timeline
January 7, 2026. We initiated contact with the Oasis team and submitted our first PGP-encrypted security report concerning Oasis ROFL. The team acknowledged receipt the same day.
January 8 to January 13, 2026. Additional encrypted reports submitted as our review expanded. By January 13, Oasis confirmed receipt of eight reports and began consolidated review.
January 26, 2026. Oasis sent its first consolidated technical response. This finding was included and acknowledged as a valid implementation gap.
February 17 to February 27, 2026. We pushed back on the severity classification, arguing that Rust-side verification paths should enforce freshness locally instead of relying only on surrounding layers. Oasis maintained their position that the practical risk was limited.
March 2, 2026. Oasis issued final classification: Low.
March 3, 2026. We accepted the classification and moved to closure.
March 5, 2026. Oasis confirmed no nondisclosure obligations and requested coordinated disclosure timing until Oasis Core 26.0 was live on mainnet. Public discussion was welcome after rollout, with attribution to Bluethroat Labs.
March 16, 2026. Bounty payment confirmed received.
Key Takeaway
When a codebase maintains parallel implementations in two languages, parity is a security property, not just a maintenance concern. The Rust module header in this case explicitly required sync with Go. The max_attestation_age field existed in the Rust struct. The freshness check existed in the Go function. Every piece was in place except the two lines that connected them. The gap was invisible unless you read both implementations side by side and compared their verification steps one by one.
If you maintain dual-language attestation verification, diff the verification steps, not just the data structures. A struct field that exists for serialization compatibility but is never read is a signal that a check was lost in translation.
References
- Affected code at time of disclosure:
- Rust
SGXAttestation::verify(): [runtime/src/consensus/registry.rs]w - Rust
SGXConstraints(deadmax_attestation_agefield): same file, lines 696-709 - Go
verifyAttestationSignature():go/common/node/sgx.go-- lines 265-287 - Go default
MaxAttestationAge:go/oasis-node/cmd/genesis/genesis.go-- line 810
- Rust
This vulnerability was discovered by Rahul Saxena of Bluethroat Labs during a security assessment of the Oasis ROFL and attestation verification surface. Responsible disclosure was coordinated with the Oasis team.
© 2026 Bluethroat Labs
