tesla

package
v0.3.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package tesla implements a TESLA-inspired hash-chain key schedule for delayed disclosure of packet authentication keys.

Key Schedule Overview

Keys form a backward hash chain: the executor generates a random secret seed k_L (the tail) and hashes it L times to obtain the public anchor k_0:

k_0 = H^L(k_L)          (public anchor, published at setup)
k_i = H(k_{i+1})        (each key is the hash of the next one)

The executor uses key k_i during epoch i (1 ≤ i < L) and discloses it once the disclosure delay of d ≥ 2 epochs has elapsed, at the start of epoch i+d (see Disclosure). Because k_0 is public, epoch 0 has no signing key: nothing is tagged before epoch 1 starts. Because k_L is never disclosed, nothing is tagged from epoch L on either. A verifier who has buffered packets from epoch i can verify them once k_i is published by checking:

H^i(k_i) == k_0

Given a disclosed key k_τ, the key for an earlier epoch t (t ≤ τ) is reconstructed by hashing forward τ-t times:

k_t = H^(τ-t)(k_τ)

Per-Measurement Derivation

Because a single executor may service multiple measurements concurrently, an additional per-measurement key ak is derived from the current chain key:

ak = HKDF-SHA256(secret=k_i, info=measurement_id, length=32)

Authentication Tag

The authentication tag written into the IPv4 IPID field follows the versioned tag specification TagSpec (docs/tag-spec.md):

tag = SipHash-2-4(ak[0:16], canonical(packet)) mod 2^16

where canonical takes the first min(64, total length) bytes and zeroes the fields a router, NAT or checksum offload rewrites. The eBPF tagger (tagger.c) and the pure-Go fallback compute it identically; see HashInput and PacketTag.

Disclosure

k_i is disclosed no earlier than the start of epoch i+d, where the disclosure delay d (Config.DisclosureDelay) is at least MinDisclosureDelay epochs. This is TESLA's safety condition: a verifier accepts k_i only for packets it received before k_i could have been disclosed, and it tries the packet's epoch t and its predecessor t-1 to absorb clock skew. With d = 1, k_{t-1} would be public during epoch t, so anyone who saw it could forge tags that verify for packets timestamped in epoch t. With d ≥ 2 the key of every candidate epoch is still secret while its packets are in flight; the margin (d-1)·I must exceed the verifier's clock tolerance plus the skew between the executor, the dispatcher and the capture host.

A key is also disclosed no earlier than the first heartbeat after every kernel tagger of the chain has moved off it. Each eBPF tagger registers with the schedule as an InstalledKeyHolder and reports the epoch whose key its map slot may still hold; DisclosedKey never names that epoch or a later one. The kernel taggers refresh their key at each epoch boundary, so in the ordinary case the taggers have moved off k_i long before epoch i+d, and a delayed or failed refresh delays disclosure instead of leaving a disclosed key installed. At the end of the chain k_{L-1} follows the same rules: it is disclosed from the start of epoch L-1+d, once every tagger has removed it at Expiry. The pure-Go tagger reads CurrentKey for every packet and holds no key.

Disclosure therefore lags the start of epoch i+d by up to one heartbeat interval, or longer while a refresh fails. A verifier waits for the key rather than treating a missing one as a failure.

Attribution

Attribution reports whether packets tagged now can be attributed: not in epoch 0 or from Expiry, not while a holder's latest refresh failed, and not once an installed key has held disclosure back for longer than one epoch. The executor sends it in its capability report.

Index

Constants

View Source
const (
	// UnattributableEpochZero: k_0 is public, so nothing sent in epoch 0, or
	// before Epoch, is tagged with a secret key.
	UnattributableEpochZero = "epoch_zero"
	// UnattributableChainExhausted: from Expiry no key signs.
	UnattributableChainExhausted = "chain_exhausted"
	// UnattributableRefreshFailing: a kernel tagger's latest key refresh
	// failed, so its slot is empty or may still hold a previous epoch's key.
	UnattributableRefreshFailing = "refresh_failing"
	// UnattributableDisclosureHeld: an installed key has held disclosure back
	// for longer than MaxDisclosureHold, so the keys of new tags stay withheld.
	UnattributableDisclosureHeld = "disclosure_held"
)

Reasons why packets tagged now cannot be attributed. They are the fixed vocabulary of the executor capability report.

View Source
const (
	UnsupportedIPv6      = tagspec.UnsupportedIPv6
	UnsupportedNotIPv4   = tagspec.UnsupportedNotIPv4
	UnsupportedTooShort  = tagspec.UnsupportedTooShort
	UnsupportedMalformed = tagspec.UnsupportedMalformed
	UnsupportedFragment  = tagspec.UnsupportedFragment
)

Reasons why a packet has no v1 tag. The tagger leaves such a packet unchanged, and a verifier reports it unsupported rather than unmatched.

View Source
const DefaultChainHorizon = 7 * 24 * time.Hour

DefaultChainHorizon is the uptime a default-sized chain covers. The default ChainLength is DefaultChainHorizon/EpochLength, so a longer epoch gives a shorter chain rather than a shorter usable lifetime.

View Source
const DefaultDisclosureWindow = 15 * time.Minute

DefaultDisclosureWindow is the wall-clock time a default disclosure delay covers: d defaults to the smallest number of epochs, and at least MinDisclosureDelay, whose length reaches it. Fifteen minutes is in the range TRACER uses (10 to 30 minutes), far beyond any verifier tolerance or disciplined clock skew, and still short enough that a result's tags become verifiable soon after the run. At the default epoch length of 10 seconds it is d = 90.

View Source
const DefaultEpochLength = 10 * time.Second

DefaultEpochLength is the epoch length I of a schedule that names none.

View Source
const MaxTagInput = tagspec.MaxInput

MaxTagInput is the most packet bytes the tag authenticates.

View Source
const MinDisclosureDelay = tagspec.MinDisclosureDelay

MinDisclosureDelay is the shortest disclosure delay d, in epochs, a schedule accepts. A verifier tries a packet's epoch t and t-1; with d = 1 the key of t-1 is already public during epoch t (see Disclosure).

View Source
const TagSpec = tagspec.ID

TagSpec identifies the packet-tag algorithm this build implements, defined in docs/tag-spec.md. Taggers and verifiers of one fleet use one version; results record it so evidence names the algorithm its tags follow.

Variables

This section is empty.

Functions

func ChainSeed

func ChainSeed(seed []byte, generation int64) ([]byte, error)

ChainSeed derives the tail k_L of one chain from a configured seed:

k_L = HKDF-SHA256(secret=seed, info="debuglet tesla chain <generation>", length=32)

Each generation of the same seed yields a different chain, so a restart with an unchanged seed does not reuse keys an earlier chain disclosed.

func ComputeTag

func ComputeTag(ak, input []byte) (uint16, error)

ComputeTag is the 16-bit tag of an already canonical input (tagspec.ComputeTag).

func DefaultDisclosureDelay added in v0.3.0

func DefaultDisclosureDelay(epochLength time.Duration) int64

DefaultDisclosureDelay returns the disclosure delay, in epochs, of a schedule with the given epoch length that names none.

func DeriveAK

func DeriveAK(k []byte, measurementID []byte) ([]byte, error)

DeriveAK computes the per-measurement authentication key:

ak = HKDF-SHA256(secret=k, info=measurementID, length=32)

measurementID is the ASCII of the run's canonical UUID (tagspec.DeriveAK).

func DeriveFromDisclosed

func DeriveFromDisclosed(disclosedKey []byte, disclosedEpoch, targetEpoch int64) ([]byte, error)

DeriveFromDisclosed reconstructs the key at targetEpoch given a disclosed key at disclosedEpoch. Because the chain runs backward (k_i = H(k_{i+1})), a verifier can only derive keys at epochs ≤ disclosedEpoch by hashing the disclosed key forward (disclosedEpoch → targetEpoch, t ≤ disclosedEpoch):

k_t = H^(disclosedEpoch - t)(k_disclosedEpoch)

To reconstruct a later (newer) key you would need an even newer disclosed key, since the chain cannot be inverted.

func HashInput added in v0.3.0

func HashInput(packet []byte) ([]byte, error)

HashInput returns the canonical tag input of an IPv4 packet (tagspec.HashInput).

func PacketTag added in v0.3.0

func PacketTag(ak, packet []byte) (uint16, error)

PacketTag is the v1 tag of an IPv4 packet under ak (tagspec.PacketTag).

func SipHash24 added in v0.3.0

func SipHash24(key, data []byte) uint64

SipHash24 is standard SipHash-2-4 (tagspec.SipHash24).

func VerifyChain

func VerifyChain(anchor, key []byte, t int64) bool

VerifyChain checks that H^t(k_t) == k_0 (the public anchor). A verifier calls this after reconstructing k_t to confirm its authenticity.

func VerifyTag

func VerifyTag(chainKey []byte, epoch int64, measurementID, packet []byte, tag uint16) (bool, error)

VerifyTag checks whether tag is the v1 tag of an IPv4 packet at a given epoch, given the chain key of that epoch and the measurement ID. Choosing the candidate epochs is the caller's (docs/tag-spec.md). A packet v1 does not cover fails with an *UnsupportedError rather than a mismatch.

Types

type Attribution added in v0.3.0

type Attribution struct {
	Epoch int64
	// Reason is empty when attribution is available, otherwise one of the
	// Unattributable constants; the first that applies in their order wins.
	Reason string
	// InstalledEpoch is the oldest epoch a registered holder may still sign
	// with; it is meaningful only when Installed.
	InstalledEpoch int64
	Installed      bool
	// LastInstall is the oldest last successful install among the holders
	// that report one, and zero when none does.
	LastInstall time.Time
	// RefreshErr is the latest error of a failing holder, nil when none fails.
	RefreshErr error
	// HeldSince is when the installed key started holding disclosure back, the
	// end of InstalledEpoch; zero when disclosure is not held.
	HeldSince time.Time
}

Attribution is whether packets tagged at one instant can be attributed once their key is disclosed, with the kernel refresh state that decides it.

type Config

type Config struct {
	// Seed is the secret tail k_L of the hash chain. If nil, a random
	// 32-byte seed is generated on the first call to NewKeySchedule.
	// The seed is NEVER disclosed; it is only used internally to derive
	// keys by hashing backwards toward k_0.
	Seed []byte

	// ChainLength L is the total number of epochs supported by this
	// schedule. The keys k_0 … k_L are generated; k_0 is the public
	// anchor and k_L is derived from the seed.
	//
	// If zero, it is derived from EpochLength so that the chain covers
	// DefaultChainHorizon of uptime. Sizing this from a wall-clock horizon
	// matters: k_L is never disclosed, so the last signing epoch is L-1 and
	// from the start of epoch L (Expiry) the schedule has no signing key and
	// nothing is tagged.
	ChainLength int64

	// EpochLength is the interval duration I of one epoch. Defaults to
	// DefaultEpochLength.
	EpochLength time.Duration

	// DisclosureDelay d is the number of epochs after which a key is
	// disclosed: k_i is disclosed once epoch i+d has started. Zero derives
	// DefaultDisclosureDelay(EpochLength); otherwise it must be at least
	// MinDisclosureDelay.
	DisclosureDelay int64

	// Epoch is the reference wall-clock time that anchors epoch 0.
	// Defaults to the time NewKeySchedule is called.
	Epoch time.Time
}

Config carries the tuneable parameters of the TESLA key schedule.

type InstalledKeyHolder added in v0.3.0

type InstalledKeyHolder interface {
	InstalledEpoch() (epoch int64, installed bool)
}

InstalledKeyHolder is a signing path that keeps a chain key outside the schedule, such as a kernel map slot. InstalledEpoch reports the epoch whose key the path may still sign with, and false once it holds no key.

type KeySchedule

type KeySchedule struct {
	// contains filtered or unexported fields
}

KeySchedule is a thread-safe TESLA hash-chain key schedule.

Keys are indexed by epoch number t where t ∈ [0, L]:

epoch t covers the time interval [Epoch + t*I, Epoch + (t+1)*I).

The chain direction is backward: k_0 is the public anchor and k_L is the private seed tail. Key k_t is used during epoch t and disclosed after the disclosure delay d has elapsed (i.e., once epoch t+d has started).

k_0 is public from setup, so it never signs: epoch 0, and any instant before Epoch, has no usable signing key. The first usable key is k_1 at Epoch+I and the last is k_{L-1}; from Expiry, the start of epoch L, no key signs.

The set of registered InstalledKeyHolders is the one mutable part; every other field is fixed at construction.

func NewKeySchedule

func NewKeySchedule(cfg Config) (*KeySchedule, error)

NewKeySchedule creates a KeySchedule from cfg.

The chain direction is backward (k_i = H(k_{i+1})). The private seed k_L is stored internally and the public anchor k_0 = H^L(seed) is computed once.

If cfg.Seed is empty a cryptographically random 32-byte seed is generated. A seed that is not exactly 32 bytes long is folded through SHA-256 so that k_L is always one key wide. If cfg.EpochLength is zero it defaults to DefaultEpochLength. If cfg.DisclosureDelay is zero it defaults to DefaultDisclosureDelay; a delay below MinDisclosureDelay is refused. If cfg.ChainLength is zero it is derived from EpochLength (see DefaultChainHorizon).

func (*KeySchedule) Anchor

func (ks *KeySchedule) Anchor() []byte

Anchor returns k_0, the public anchor that should be published at setup. Verifiers use it to check consistency: H^t(k_t) == k_0.

func (*KeySchedule) Attribution added in v0.3.0

func (ks *KeySchedule) Attribution(now time.Time) Attribution

Attribution reports the state at now. Without a registered holder only the epoch decides it, which is also the pure-Go tagger's case.

func (*KeySchedule) ChainLength

func (ks *KeySchedule) ChainLength() int64

ChainLength returns L, the highest epoch this schedule can serve.

func (*KeySchedule) ComputeTagForPacket

func (ks *KeySchedule) ComputeTagForPacket(t time.Time, measurementID, packet []byte) (uint16, error)

ComputeTagForPacket derives ak from the chain key at time t and returns the v1 tag of packet. It fails while no chain key is usable and with an *UnsupportedError for a packet v1 does not tag.

func (*KeySchedule) Config

func (ks *KeySchedule) Config() Config

Config returns a copy of the schedule's configuration.

func (*KeySchedule) CurrentKey

func (ks *KeySchedule) CurrentKey(t time.Time) []byte

CurrentKey returns the chain key k_t for the epoch that contains time t, or nil while no key is usable: epochOf maps epoch 0 and any instant before Epoch to 0, whose key is the public anchor (see KeySchedule), and every instant from the start of epoch L to L, whose key k_L is never disclosed. Every signing path reads the key here, so this is the one place the rule is decided.

func (*KeySchedule) DisclosedKey

func (ks *KeySchedule) DisclosedKey(t time.Time) (index int64, key []byte, ok bool)

DisclosedKey returns the epoch index and key that should be disclosed at time t. The key for epoch τ is disclosed once the disclosure delay of d epochs has elapsed after τ: at the start of epoch τ+d, so at time t the disclosed epoch is epoch(t)−d.

Before epoch d has started (no key is disclosable yet), ok is false. The disclosed key stops at k_{L-1}, the last signing key, once epoch L−1+d has started, even though the chain was exhausted at epoch L; k_L is never disclosed.

A registered holder that may still sign with k_e caps the result at e-1, so a key is never disclosed while an installed copy of it can tag packets.

func (*KeySchedule) DisclosureDelay added in v0.3.0

func (ks *KeySchedule) DisclosureDelay() int64

DisclosureDelay returns d, the number of epochs after which a key is disclosed.

func (*KeySchedule) EpochOf added in v0.3.0

func (ks *KeySchedule) EpochOf(t time.Time) int64

EpochOf returns the epoch that contains t: 0 before Epoch, and at most L.

func (*KeySchedule) Exhausted

func (ks *KeySchedule) Exhausted(t time.Time) bool

Exhausted reports whether time t falls at or past the end of the chain, the start of epoch L. k_L is never disclosed, so an exhausted schedule has no signing key and CurrentKey returns nil from then on.

func (*KeySchedule) Expiry

func (ks *KeySchedule) Expiry() time.Time

Expiry returns the wall-clock time at which the chain runs out.

func (*KeySchedule) FinalDisclosure added in v0.3.0

func (ks *KeySchedule) FinalDisclosure() time.Time

FinalDisclosure returns when the last signing key k_{L-1} becomes disclosable: the start of epoch L-1+d, d-1 epochs after Expiry. Keys live only in this schedule, so a restart before then starts a new chain and the keys of the old chain's last undisclosed epochs are never published; the packets they tagged cannot be verified.

func (*KeySchedule) KeyAtEpoch

func (ks *KeySchedule) KeyAtEpoch(epoch int64) ([]byte, error)

KeyAtEpoch returns the chain key for the given epoch index.

func (*KeySchedule) MaxDisclosureHold added in v0.3.0

func (ks *KeySchedule) MaxDisclosureHold() time.Duration

MaxDisclosureHold bounds how long an installed key may hold disclosure back before attribution is reported unavailable. One epoch covers the boundary refresh and its first retry at EpochLength/2; the hold at every boundary until the refresh lands is ordinary and stays well inside it.

func (*KeySchedule) RegisterInstalled added in v0.3.0

func (ks *KeySchedule) RegisterInstalled(h InstalledKeyHolder)

RegisterInstalled adds h to the holders that cap DisclosedKey. A holder registers before it installs its first key.

func (*KeySchedule) UnregisterInstalled added in v0.3.0

func (ks *KeySchedule) UnregisterInstalled(h InstalledKeyHolder)

UnregisterInstalled removes h once it can no longer sign with any key.

type RefreshReporter added in v0.3.0

type RefreshReporter interface {
	LastRefresh() (installed time.Time, err error)
}

RefreshReporter is an InstalledKeyHolder that also reports its refresh: the time of its last successful key install, zero before one, and the error of its latest attempt, nil after a success.

type UnsupportedError added in v0.3.0

type UnsupportedError = tagspec.UnsupportedError

UnsupportedError is returned for a packet the specification assigns no tag.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL