Skip to content

Fix enclave clock drift: sync CLOCK_REALTIME to the hypervisor PTP clock (/dev/ptp0) #137

Description

@aruokhai

Problem

An AWS Nitro Enclave's clock (kvm-clock) is set once at boot from the Nitro Hypervisor and never re-synchronized afterward; it drifts ~1s/day proportional to load (AWS-confirmed). There is no time synchronization in the codebase, so the drift silently corrupts every wall-clock decision the runtime makes:

  • Freshness anchor rollback records — time.Now().Unix() + S3 Object-Lock RetainUntilDate (runtime/anchor.go)
  • PCR0 lineage audit timestamps + Object-Lock retain-until (runtime/lineage.go)
  • Self-signed TLS cert NotBefore/NotAfter (runtime/runtime.go)
  • K/V TTL expiry, stream XADD IDs, migration cooldown (runtime/kvstore.go, runtime/resp.go, runtime/migrate.go)

Fix

Sync CLOCK_REALTIME to the hypervisor's PTP hardware clock (/dev/ptp0, the built-in ptp_kvm driver) for the enclave's lifetime:

  • (*Runtime).StartClockSync() (runtime/runtime.go) opens /dev/ptp0, hard-steps CLOCK_REALTIME to it once at boot, then spawns the slew loop. Wired into runtime/cmd/runtime/main.go before any outbound TLS / listeners.
  • runClockSync() (runtime/clocksync.go) reads the PTP + system clocks every 5s and, when drift exceeds 5ms, slews gradually via unix.ClockAdjtime (ADJ_OFFSET_SINGLESHOT) — never a backward jump, safe for the runtime's near-monotonic timestamp consumers. Exits on the lifecycle stop channel.
  • Uses already-vendored golang.org/x/sys/unix (ClockGettime/ClockSettime/ClockAdjtime) — no new dependency, no vendorHash change. Runs as root → CAP_SYS_TIME present.
  • Graceful no-op if /dev/ptp0 is absent (logs clock sync disabled: /dev/ptp0 unavailable and returns).

Why this is host-safe

The enclave's time comes from the Nitro Hypervisor (/dev/ptp0/kvm-clock), which sits outside the parent EC2 instance's trust boundary. A malicious host cannot forge or rewind the clock — setting the parent OS clock has zero effect, and it can't substitute a fake PTP device (the enclave kernel reads it via a KVM hypercall the host isn't in the path of). Across restarts the new enclave re-reads the hypervisor's current time. The host's only residual is delay/DoS, already in the threat model.

Feasibility (verified)

The EIF's kernel is aws/aws-nitro-enclaves-cli v1.2.3 (pinned via monzo/aws-nitro-util). Its bzImage.config has CONFIG_PTP_1588_CLOCK_KVM=y (built-in), CONFIG_PTP_1588_CLOCK=y, CONFIG_KVM_GUEST=y, and CONFIG_DEVTMPFS_MOUNT=y — so /dev/ptp0 is auto-created at boot on real Nitro. No rootfs/kernel/mknod work needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions