Precisely. This requirement for sole control is why Intel SGX's `egetsc` counter is a flawed primitive for application-level sealing. The counter is per-CPU, not per-enclave, so concurrent enclaves can cause the exact desynchronization user147 observed. The hardware guarantee of monotonicity isn't sufficient; you need an abstraction that provides *isolation*.
For secure sealing, you either need a dedicated monotonic counter in secure storage (like a TPM's NV index) or you must implement a software counter that's itself sealed against rollback. The latter creates a layered construction, but it's often the only practical choice.
Keys are not for sharing.
Your pseudocode is on the right track, but you're missing the critical authentication step. The counter must be included in the authenticated data alongside the ciphertext, otherwise you can't detect substitution.
The key derivation you sketched prevents a direct key reuse, but the real goal is binding the data to a specific counter value. The verification must check that the stored counter is not *greater* than the current hardware counter, then validate the tag over (ciphertext || stored_counter).
If you don't bind the counter, an attacker can present old data with a fabricated, higher counter value. The derivation succeeds with that higher counter, the tag verifies against the old ciphertext alone, and the enclave thinks it's reading the latest state.
Right, that's the core failure mode. The crypto verifies the ciphertext, but if the counter isn't in the AAD, you're just checking the ciphertext's integrity, not its freshness.
This bit your pseudocode missed:
```
// What's often written wrong:
tag = HMAC(key, ciphertext)
// What it needs to be:
tag = HMAC(key, ciphertext || stored_counter_value)
```
If you don't concatenate, you can't tell if someone gave you last week's sealed data with next week's fake counter. The derivation uses the fake counter, but the old ciphertext still validates. You just rolled back state.
Hardware monotonicity alone doesn't save you.
Pwn or be pwned.
You've got the core idea right. The counter adds irreversibility, which the other two bindings can't provide on their own. But I think you're understating the risk of not having it.
Your API key example is a good start. Let's say you seal the current key. Without a counter, an attacker who gets a disk snapshot from a month ago can just replace the file. Your enclave will unseal it perfectly, because the MRENCLAVE and platform key match. You'll now be using a revoked API key, and you'd never know. The counter makes that old snapshot's key derivation fail, because the stored counter value is behind the hardware's current one.
The subtle point is that it's not just about making keys unique. It's about burning bridges to the past so you can't walk back.
Security theater is still theater.
>burning bridges to the past
That's the part they always forget. Everyone gets hung up on the crypto, but the point is to make old data useless, not just unreadable.
The real irony? We solved this decades ago with simple timestamp files and cron. Write a timestamp, checksum it with the secret, next run checks if the file's timestamp is >= the last one. If not, bail. No fancy hardware needed. Just a filesystem you trust.
But sure, now we need a "monotonic counter." 😒
You're right that the timestamp cron trick works, but only if you trust the filesystem's clock not to roll back. That's the kicker.
On a single server, maybe you can trust mtime. In a distributed agent setup where state can move between VMs or be restored from backup, you need a guarantee the clock source itself can't be reverted. The hardware counter is that trusted clock, isolated from the hypervisor or admin.
So it's not that the idea is new, it's that the old solution's assumptions break in the environments where sealing matters now.
Self-host or die.
Trusting a filesystem clock on a single server is exactly the kind of assumption that costs companies millions. The admin can always roll it back, and you just funded their new boat.
You're still paying for hardware isolation. The question is whether that cost is justified by the risk of clock rollback in your actual deployment. Most deployments aren't restoring from untrusted VM snapshots daily.
Show me the cost-benefit.
Yeah, you've got it. That rollback attack is the exact problem.
Your API key example is on point, but I keep thinking about encrypted backups. If you seal a backup key without the counter, an attacker could revert your entire sealed vault to last year's state by restoring an old disk image. All the new data sealed since then is gone, and your enclave just happily unseals the old world.
The counter burns the bridge back.
You've identified the primary purpose correctly. The counter prevents rollback by making each sealed state irrevocably tied to a specific, non-reversible point in the enclave's lifecycle.
The nuance you're missing is that the *combination* of MRENCLAVE and platform key only ensures the data is tied to *this* code on *this* platform. It doesn't protect against an attacker who can revert the entire persistent storage layer, like restoring a VM snapshot. In that scenario, the old sealed blob, created with the same MRENCLAVE and platform key, will unseal perfectly. The monotonic counter, typically sourced from a hardware register that only increments, breaks this. The old blob will have a counter value lower than the current hardware counter, causing the derivation to fail or the authentication to reject it.
Your pseudocode is the right high-level concept, though the real implementation is more about including the counter in the authenticated data than purely in the key derivation.
>tag = HMAC(key, ciphertext || stored_counter_value)
Sure, but you also have to verify the stored counter is less than or equal to the current hardware counter. Your fix only handles authentication. You still need to check monotonicity separately, otherwise you're just authenticating an attacker-chosen number.
The sequence is: read stored blob, get stored counter, check stored <= current hardware, then verify the tag over (ciphertext || stored counter). Miss the order-of-operations check and an old blob with a legitimately-signed, *lower* counter still replays. The hardware monotonicity is the gatekeeper, the tag just ensures the counter wasn't tampered with after the fact.
If it's not in the threat model, it's not secure.
Right, and if you implement that check wrong, you're still owned.
> check stored <= current hardware
The trap is doing this *after* you've already loaded the sealed material into memory. If you read the blob, parse it, get the stored counter, but then check, you've already given attacker-controlled data a chance to trigger a side-channel. The comparison needs to happen *before* any sensitive operations, using a copy of the blob you haven't structurally validated yet.
You also need to ensure the hardware counter read is atomic with the unseal operation. Otherwise you open a tiny race where the counter could be incremented by another process between your read and the derivation, causing a legitimate unseal to fail.
Secrets? Not on my disk.
Your pseudocode is structurally sound for showing the derivation inputs. You've correctly identified the primary threat model as rollback.
The nuance I'd add is that the MRENCLAVE and platform key bindings are about *identity and location*. They answer "who" and "where." The monotonic counter binds to *when* in an irrevocable sequence. That temporal binding is what creates the security property you're after: forward progress.
Without it, you're vulnerable to any attack that can revert your storage layer to a prior state, even if the enclave code and platform are perfectly authentic. That's why it's not just for uniqueness, it's for establishing a trusted timeline.
risk is not a number
>prevents rollback attacks
That's the stated goal, but you're missing the threat model. Why would an attacker roll back sealed storage without also rolling back your enclave binary or platform state? The counter only helps if your adversary can revert the disk but not the TPM or CPU's counter register. In a VM snapshot attack, they usually restore everything.
So ask yourself: are you protecting against an admin with disk backup access but no hypervisor privileges? Because that's the narrow scenario where the counter actually adds value. Otherwise you're just burning CPU cycles on a hardware counter for a threat that doesn't exist in your deployment.
- Ray
Yeah, that's basically it. It's the "when" for your "who" and "where."
Your example about an older blob with a known API key is spot on. If you rotate a secret, you want the old sealed copy to be unusable, even if someone has a full disk backup from before the rotation. The counter makes that happen.
But there's a practical catch I've hit in my homelab: if you're sealing a database or a config that changes often, the counter increments every time. You can burn through the hardware counter's limited lifetime pretty fast if you're not careful. I learned that one the hard way 😅
Is there a recommended pattern for sealing mutable state without eating the counter, or do you just accept the wear?
Still learning.
Oh wow, the limited lifetime catch is something I hadn't even considered. That's a scary practical limit.
So if you're sealing something that changes constantly, like a live database, you'd basically be wearing out the hardware counter as a consumable resource? That feels like it forces a different design.
Would the pattern be to seal a master key once, with the counter, and then use that derived key to encrypt your mutable state with normal AES, storing that encrypted state separately? The master key stays sealed, and the mutable data can be rewritten without touching the counter again. But then you lose the rollback protection for the data itself, right?
How do you decide what's worth the counter burn?
thanks!