Documentation
¶
Overview ¶
Package session implements SEMP session lifecycle management: session state, key derivation handoff from the handshake, TTL bookkeeping, proactive rekeying via SEMP_REKEY, the expiry log used for replay prevention, and the concurrent-session bounds enforced by servers.
Sessions provide forward secrecy. The ephemeral private keys exchanged in the handshake are erased immediately after the shared secret is computed, so a future compromise of any long-term key cannot retroactively decrypt past sessions.
Specification reference: SESSION.md.
Index ¶
- Constants
- Variables
- func Dispatch(ctx context.Context, stream MessageStream, h DispatchHandlers) error
- func OpenRekeyMessage(suite crypto.Suite, s *Session, sealed *SealedRekey) ([]byte, error)
- type Bounds
- type DispatchHandlers
- type EphemeralKey
- type ExpiryLog
- type MessageStream
- type RekeyAccepted
- type RekeyHandler
- type RekeyInit
- type RekeyRejected
- type RekeyStream
- type Rekeyer
- type Role
- type SealedRekey
- type Session
- func (s *Session) AcceptsID(sessionID string, now time.Time) bool
- func (s *Session) AcceptsIDWithGrace(sessionID string, now time.Time, grace time.Duration) bool
- func (s *Session) Active(now time.Time) bool
- func (s *Session) ActiveWithGrace(now time.Time, grace time.Duration) bool
- func (s *Session) ApplyRekey(newID string, newKeys *crypto.SessionKeys, now time.Time)
- func (s *Session) CanRekey(now time.Time) (bool, string, string)
- func (s *Session) CanRekeyWithGrace(now time.Time, grace time.Duration) (bool, string, string)
- func (s *Session) EncC2S() []byte
- func (s *Session) EncS2C() []byte
- func (s *Session) EnvMAC() []byte
- func (s *Session) Erase()
- func (s *Session) MACC2S() []byte
- func (s *Session) MACS2C() []byte
- func (s *Session) PreviousEnvMAC(now time.Time) []byte
- func (s *Session) Resumption() []byte
- func (s *Session) SetKeys(k *crypto.SessionKeys)
- type State
- type StatelessTicketIssuer
- func (s *StatelessTicketIssuer) Consume(_ context.Context, ticket []byte) error
- func (s *StatelessTicketIssuer) Issue(_ context.Context, identity string, resumptionSecret []byte, ...) ([]byte, error)
- func (s *StatelessTicketIssuer) Open(_ context.Context, ticket []byte, now time.Time) (string, []byte, time.Time, error)
- func (s *StatelessTicketIssuer) PruneConsumed(now time.Time)
- type TicketIssuer
Constants ¶
const ( StepRekeyInit = "init" StepRekeyAccepted = "accepted" StepRekeyRejected = "rejected" )
Rekey steps (HANDSHAKE.md §1.4 SEMP_REKEY discriminator).
const ( DirectionC2S = "c2s" DirectionS2C = "s2c" )
Direction values.
const MaxRekeysPerSession = 10
MaxRekeysPerSession is the upper bound on rekey events per session lifetime per SESSION.md §3.5.
const MaxTicketLifetime = 7 * 24 * time.Hour
MaxTicketLifetime is the upper bound on a resumption ticket's expires_at relative to its issuance time, per HANDSHAKE.md §2.8.4 and SESSION.md §2.7. Issuers MUST cap any longer requested expires_at to this value; openers MUST reject tickets whose stored expires_at exceeds (issued_at + MaxTicketLifetime + a grace margin).
const MessageType = "SEMP_REKEY"
MessageType is the wire-level type discriminator for rekey messages.
const MinRekeyInterval = 1 * time.Minute
MinRekeyInterval is the minimum spacing between rekey events per SESSION.md §3.5.
const RekeyThreshold = 0.8
RekeyThreshold is the fraction of TTL at which clients SHOULD initiate rekeying. SESSION.md §3.1 recommends 80%.
const TicketIDLen = 16
TicketIDLen is the length in bytes of the cleartext ticket identifier prefix used by stateless tickets to key the consumed-ticket cache. 16 random bytes; collision probability 2^-64 per ticket-id is negligible at any realistic issuance rate.
const TransitionWindow = 5 * time.Second
TransitionWindow is the duration during which both the old and the new session_id are accepted after a successful rekey (SESSION.md §3.4).
Variables ¶
var ErrTicketConsumed = errors.New("session: ticket already consumed")
ErrTicketConsumed is returned when a presented ticket has already been recorded in the consumed-ticket cache.
var ErrTicketExpired = errors.New("session: ticket expired")
ErrTicketExpired is returned when a presented ticket's expires_at is in the past (subject to clock-skew tolerance applied by the caller; the issuer itself does strict expiry).
var ErrTicketUnknown = errors.New("session: ticket unknown or corrupt")
ErrTicketUnknown is returned when a presented ticket cannot be opened: corrupt, decrypted-payload-malformed, or (for stateful implementations) not in the table.
Functions ¶
func Dispatch ¶ added in v0.4.3
func Dispatch(ctx context.Context, stream MessageStream, h DispatchHandlers) error
Dispatch reads frames from stream and routes each to the matching handler in h. Returns io.EOF on clean stream close; returns the underlying error on transport failure or context cancellation.
Dispatch is the post-handshake message-loop primitive. Higher-level runtimes (a server's per-session goroutine, a client's reactive event loop) call this with a configured DispatchHandlers; the per-type handlers themselves are caller-supplied.
Dispatch is transport-agnostic - it consumes any MessageStream and never owns the underlying connection. The caller closes the stream when Dispatch returns.
func OpenRekeyMessage ¶
OpenRekeyMessage reverses SealRekeyMessage: it decrypts the sealed blob under the session's directional keys and returns the plaintext JSON body. The caller parses the body into the appropriate RekeyInit / RekeyAccepted / RekeyRejected struct.
OpenRekeyMessage is direction-aware: when processing a message sent in the c2s direction, the RECEIVER (server) still uses the c2s key pair to decrypt, because both sides agree on which directional keys name which half of the session. sealed.Direction tells the receiver which pair to use.
Types ¶
type Bounds ¶
type Bounds struct {
// MaxClientSessions is the maximum number of concurrently active
// client sessions across all users. Default 10,000.
MaxClientSessions int
// MaxFederationSessions is the maximum number of concurrently active
// federation sessions across all peers. Default 1,000.
MaxFederationSessions int
}
Bounds defines the per-server concurrent session limits enforced via SESSION.md §2.5.3.
func DefaultBounds ¶
func DefaultBounds() Bounds
DefaultBounds returns the recommended defaults from SESSION.md §2.5.3.
type DispatchHandlers ¶ added in v0.4.3
type DispatchHandlers struct {
// OnEnvelope handles a SEMP_ENVELOPE submission frame.
OnEnvelope func(ctx context.Context, raw []byte) error
// OnRekey handles a SEMP_REKEY frame.
OnRekey func(ctx context.Context, raw []byte) error
// OnKeys handles a SEMP_KEYS request or response frame.
OnKeys func(ctx context.Context, raw []byte) error
// OnFetch handles a SEMP_FETCH inbox-pull frame.
OnFetch func(ctx context.Context, raw []byte) error
// OnDiscovery handles a SEMP_DISCOVERY exchange frame.
OnDiscovery func(ctx context.Context, raw []byte) error
// OnDelivery handles SEMP_DELIVERY_ACK / SEMP_DELIVERY_RECEIPT
// frames.
OnDelivery func(ctx context.Context, raw []byte) error
// OnUnknown is invoked when a frame's `type` field does not match
// any registered handler. Default is "silently drop" per the
// protocol forward-compatibility rule.
OnUnknown func(ctx context.Context, msgType string, raw []byte) error
// OnHandlerError is invoked on a non-fatal error from a registered
// handler. The dispatch loop continues; the caller decides whether
// to close the stream. Default is "swallow" - handler errors are
// non-fatal by design.
OnHandlerError func(err error, msgType string)
}
Handlers maps SEMP message types to per-message handler callbacks. A Dispatch loop reads frames from a MessageStream, parses the outer `type` field, and routes each frame to the matching handler.
Handlers are protocol-pure: they accept raw bytes and return on completion or with a transport-level error. Application code composes Handlers with the per-type primitives this library exposes (envelope.Decode, keys.HandleRequest, session.RekeyResponder, etc.) to assemble a full server runtime.
A Handler that is not registered for a given type is treated per the protocol's forward-compatibility rule: the frame is silently dropped unless [DispatchHandlers.OnUnknown] is set, in which case it is invoked with the parsed type string and raw frame.
type EphemeralKey ¶
type EphemeralKey struct {
Algorithm string `json:"algorithm"`
Key string `json:"key"`
KeyID string `json:"key_id"`
}
EphemeralKey is the new ephemeral public key offered or accepted in a rekey exchange.
type ExpiryLog ¶
type ExpiryLog interface {
// Retire records that the given session_id has been retired at the
// given time.
Retire(ctx context.Context, sessionID string, retiredAt time.Time) error
// Retired reports whether the given session_id is in the expiry log
// (i.e. it has been retired and the entry has not yet aged out).
Retired(ctx context.Context, sessionID string) (bool, error)
// Sweep removes entries older than now - retention. Implementations
// SHOULD call Sweep periodically to bound memory usage.
Sweep(ctx context.Context, now time.Time, retention time.Duration) error
}
ExpiryLog records session IDs that have expired or been invalidated. Receiving servers consult the expiry log when verifying postmark session_id values: a session_id that names a retired session MUST cause the envelope to be rejected with reason code handshake_invalid (SESSION.md §5.2).
Entries are retained for the maximum postmark.expires window (default one hour per ENVELOPE.md §10.2). Only the session_id and its retirement timestamp are kept; no key material is retained.
type MessageStream ¶ added in v0.4.3
type MessageStream interface {
Send(ctx context.Context, msg []byte) error
Recv(ctx context.Context) ([]byte, error)
}
MessageStream is the minimal interface Dispatch needs from a transport. transport.Conn satisfies it; tests can substitute an in-memory channel pair without pulling in the transport package.
type RekeyAccepted ¶
type RekeyAccepted struct {
Type string `json:"type"`
Step string `json:"step"` // StepRekeyAccepted
Version string `json:"version"`
SessionID string `json:"session_id"`
NewSessionID string `json:"new_session_id"`
NewEphemeralKey EphemeralKey `json:"new_ephemeral_key"`
RekeyNonce string `json:"rekey_nonce"`
ResponderNonce string `json:"responder_nonce"`
}
RekeyAccepted is the responder's acceptance of a rekey exchange.
type RekeyHandler ¶
type RekeyHandler struct {
Suite crypto.Suite
Session *Session
// InitiatorDirection identifies which half of the session keys
// the INITIATOR uses. The handler uses the opposite direction to
// seal its response. Zero means DirectionC2S (the client-
// initiated rekey default), so the handler seals its response
// under DirectionS2C.
InitiatorDirection string
}
RekeyHandler runs the responder side of a SEMP_REKEY exchange. It is invoked with an already-received (sealed) SEMP_REKEY byte slice which the dispatch loop has just read off the stream. It writes either a sealed RekeyAccepted or a sealed RekeyRejected back to stream.
On success, the supplied *Session is mutated via ApplyRekey so that subsequent operations use the new keys. On rejection, the session is left untouched.
func (*RekeyHandler) Handle ¶
func (h *RekeyHandler) Handle(ctx context.Context, stream RekeyStream, raw []byte) error
Handle processes one sealed SEMP_REKEY message and writes the response. Returns nil on either rekey_accepted or rekey_rejected (both are "handled" outcomes); returns a non-nil error only on transport-level or unseal failures that prevent a response from being written.
type RekeyInit ¶
type RekeyInit struct {
Type string `json:"type"` // always SEMP_REKEY
Step string `json:"step"` // always StepRekeyInit
Version string `json:"version"`
SessionID string `json:"session_id"`
NewEphemeralKey EphemeralKey `json:"new_ephemeral_key"`
RekeyNonce string `json:"rekey_nonce"`
}
RekeyInit is the rekey-init message sent by the initiating party to rotate session keys without a full re-authentication (SESSION.md §3.2).
The message is encrypted under the current K_enc_*2* and MACed under the corresponding K_mac_*2* before transmission. Possession of the current session keys is the only authentication required.
type RekeyRejected ¶
type RekeyRejected struct {
Type string `json:"type"`
Step string `json:"step"` // StepRekeyRejected
Version string `json:"version"`
SessionID string `json:"session_id"`
ReasonCode string `json:"reason_code"`
Reason string `json:"reason"`
}
RekeyRejected is the responder's rejection of a rekey exchange. The reason code is one of session_expired, rekey_unsupported, or rate_limited per SESSION.md §3.2.
type RekeyStream ¶
type RekeyStream interface {
Send(ctx context.Context, msg []byte) error
Recv(ctx context.Context) ([]byte, error)
}
RekeyStream is the minimal message-stream interface a rekey driver needs from a transport. transport.Conn and the handshake / MessageStream interfaces all satisfy it structurally.
type Rekeyer ¶
type Rekeyer struct {
// Suite is the negotiated cryptographic suite. Must match the one
// used to establish the session.
Suite crypto.Suite
// Session is the session to rekey. On success, Session.ApplyRekey
// is called with the new keys, the new ID, and the current time.
Session *Session
// InitiatorDirection identifies which half of the session keys the
// initiator uses to encrypt its rekey messages per SESSION.md §3.2.
// For a client-initiated rekey this is DirectionC2S; for a
// federation-initiated rekey the initiating server uses whichever
// half of the session it "owns" (by convention, the side that
// opened the connection uses c2s).
//
// A zero value defaults to DirectionC2S so existing callers that
// drive a client session keep working without changes.
InitiatorDirection string
}
Rekeyer runs the client-initiated side of a SEMP_REKEY exchange (SESSION.md §3). It is stateful: each call to Rekey consumes one rekey slot on the session.
Usage:
r := &session.Rekeyer{Suite: suite, Session: sess}
err := r.Rekey(ctx, stream)
if err == nil {
// sess.ID is now the new session ID
// sess.PreviousID holds the previous one during the transition window
}
func (*Rekeyer) Rekey ¶
func (r *Rekeyer) Rekey(ctx context.Context, stream RekeyStream) error
Rekey executes the two-message SEMP_REKEY exchange over stream:
- Generate a fresh ephemeral key pair and rekey nonce.
- Seal the RekeyInit under K_enc_{initiator direction} and send.
- Receive the sealed response, open it under the opposite directional keys, parse the inner RekeyAccepted (or RekeyRejected).
- Compute the shared secret via X25519 against the responder's ephemeral public key.
- Derive the five new session keys via crypto.DeriveRekeyKeys with salt = rekey_nonce || responder_nonce.
- Call Session.ApplyRekey to install the new keys and ID.
Both messages are AEAD-sealed under the current session's directional keys per SESSION.md §3.2. The AEAD additional data binds each ciphertext to the direction label, the current session ID, and the corresponding MAC key, so an attacker who somehow extracted the encryption key alone still could not forge a message - the MAC key is mixed into the AAD.
Rekey is NOT invoked automatically - callers decide when to rekey (typically at 80% TTL per SESSION.md §3.1).
Reference: SESSION.md §3.2 - §3.5.
type Role ¶
type Role int
Role identifies whether this session represents a client connection to a home server or a federation connection to a peer domain.
type SealedRekey ¶
type SealedRekey struct {
// Type is always MessageType ("SEMP_REKEY") - the session dispatch
// loop uses this to route sealed rekey messages through the rekey
// handler.
Type string `json:"type"`
// Sealed is true for encrypted messages. A future revision might
// allow cleartext rekey during upgrade, which is why this is
// explicit rather than implicit.
Sealed bool `json:"sealed"`
// Direction is "c2s" or "s2c" - selects which pair of session
// keys to use.
Direction string `json:"direction"`
// Version is the protocol version.
Version string `json:"version"`
// Nonce is the base64-encoded AEAD nonce.
Nonce string `json:"nonce"`
// Ciphertext is the base64-encoded AEAD ciphertext || auth tag.
Ciphertext string `json:"ciphertext"`
}
SealedRekey is the wire-level envelope that carries an AEAD-encrypted SEMP_REKEY message over the authenticated session channel (SESSION.md §3.2: "Both messages are encrypted and MACed using the current session keys").
The dispatch loop recognizes a rekey message by the top-level `type` field; the actual RekeyInit / RekeyAccepted / RekeyRejected body is JSON-encoded, AEAD-sealed under the current K_enc_*2*, and base64- encoded into Ciphertext. The AEAD's own tag serves as the MAC - the spec's "MACed under the corresponding MAC key" language is satisfied by including the MAC key in the AEAD additional data, which binds the ciphertext to both keys simultaneously.
Direction is "c2s" when the initiator is the client half of the session and "s2c" when it is the server half. A receiver uses Direction to pick the right pair of (encryption key, MAC key) to open the message. Tampering with Direction causes decryption to fail because the AAD changes.
func SealRekeyMessage ¶
func SealRekeyMessage(suite crypto.Suite, s *Session, direction string, plaintext []byte) (*SealedRekey, error)
SealRekeyMessage encrypts the JSON-encoded rekey message `plaintext` under the session's directional encryption key, binding the MAC key as AEAD additional data. The result is a SealedRekey ready to be marshaled and sent.
direction MUST be DirectionC2S when the caller is the initiator's client side (or, for federation, the side that opened the connection), and DirectionS2C otherwise.
type Session ¶
type Session struct {
// ID is the server-generated session identifier (ULID recommended).
ID string
// Role is RoleClient or RoleFederation.
Role Role
// State is the current lifecycle phase.
State State
// EstablishedAt is the wall-clock time at which the session was
// accepted (the moment the responder sent step "accepted"). Per
// SESSION.md §3.4, this timestamp is inherited across rekeys.
EstablishedAt time.Time
// TTL is the session lifetime in seconds, taken from the responder's
// session_ttl field.
TTL time.Duration
// ExpiresAt is the locally computed EstablishedAt + TTL (reset to
// now + TTL on every successful rekey per SESSION.md §3.4).
ExpiresAt time.Time
// PeerIdentity is the authenticated peer identifier: a user address for
// client sessions, a domain for federation sessions.
PeerIdentity string
// RekeyCount is the number of successful rekey events this session
// has seen. SESSION.md §3.5 caps this at MaxRekeysPerSession (10);
// an 11th rekey MUST be rejected with reason_code rate_limited.
RekeyCount int
// LastRekeyAt is the wall-clock time of the most recent successful
// rekey (zero if none yet). SESSION.md §3.5 requires at least one
// minute between rekey attempts.
LastRekeyAt time.Time
// PreviousID is the session's prior ID during the brief transition
// window after a rekey (SESSION.md §3.4). Envelopes whose postmark
// references PreviousID are still accepted until PreviousIDExpiresAt.
// Empty outside of that window.
PreviousID string
// PreviousIDExpiresAt is the deadline after which PreviousID is no
// longer accepted. Zero when PreviousID is empty.
PreviousIDExpiresAt time.Time
// contains filtered or unexported fields
}
Session is the in-memory state for a single SEMP session, mirroring the fields enumerated in SESSION.md §2.3 (server side) and §2.6.1 (client side). The struct is intentionally not safe for JSON marshaling: there is no MarshalJSON shim and the key fields are unexported, because session state MUST NOT be written to disk, replicated, or included in backups (SESSION.md §2.3, §2.6.2).
func New ¶
New constructs a Session with State StateInitial. Other fields are filled in by the handshake state machine as it progresses.
func (*Session) AcceptsID ¶
AcceptsID reports whether sessionID matches the session's current or transition-window previous ID at the given wall-clock time. Used by inbound envelope processing during the brief transition window after a rekey (SESSION.md §3.4).
AcceptsID applies the strict transition-window check (no grace). The TransitionWindow is already a deliberately short 5-second budget tied to a local rekey decision, so adding a peer-clock grace would balloon it beyond design intent. Receivers that want peer-clock-skew tolerance for the PRIOR id specifically use AcceptsIDWithGrace.
func (*Session) AcceptsIDWithGrace ¶ added in v0.4.0
AcceptsIDWithGrace is the tolerance-aware variant of AcceptsID. grace extends the post-rekey transition window's tail by up to the supplied amount so a slow-clock peer can still address the PRIOR id for a short period after the strict TransitionWindow has elapsed.
The grace applies ONLY to PreviousIDExpiresAt; the current id match returns true unconditionally. A negative grace is treated as zero.
Most callers do NOT want this variant - the TransitionWindow is already a peer-aware budget tied to the rekey roundtrip. The helper exists for receivers that want to absorb peer-clock skew at the session-id boundary specifically (rare in practice).
func (*Session) Active ¶
Active reports whether the session is currently usable for sending envelopes (State == StateActive and now < ExpiresAt). This is the strict, sender-side check per CONFORMANCE.md §9.3.1's "senders MUST NOT rely on grace windows": a sender treats the session as expired the moment its clock crosses ExpiresAt.
Receivers that want the SHOULD/MAY grace window (accept envelopes for up to 15 minutes past ExpiresAt to absorb peer-clock skew) use ActiveWithGrace.
func (*Session) ActiveWithGrace ¶ added in v0.4.0
ActiveWithGrace is the receiver-side variant of Active: the session is treated as still active when now is at or before ExpiresAt + grace. Per §4.4 / CONFORMANCE.md §9.3.1, receivers MAY accept envelopes under a session for up to clockskew.Default (15 minutes) past ExpiresAt to absorb peer-clock skew, while senders treat the session as expired immediately.
A negative grace is treated as zero (the strict Active semantics).
func (*Session) ApplyRekey ¶
ApplyRekey swaps in the new session keys produced by a successful rekey exchange. It also:
- retires the current ID into PreviousID (with a TransitionWindow grace period during which the old ID still matches per §3.4),
- sets the current ID to newID,
- resets ExpiresAt to now + original TTL (§3.4),
- increments RekeyCount and sets LastRekeyAt.
The caller is responsible for erasing the PRIOR keys before passing the new ones in - SetKeys does not erase the previous value because different callers have different erase policies (e.g. tests prefer to inspect both sets).
func (*Session) CanRekey ¶
CanRekey reports whether s may be rekeyed at wall-clock time `now` under SESSION.md §3.5: the session must be active, at least MinRekeyInterval must have elapsed since the last rekey, and the session must not have exceeded MaxRekeysPerSession events.
Returns (ok, reasonCode, reason). The reasonCode is one of "session_expired", "rate_limited", or empty on success.
CanRekey applies the strict, sender-side expiry check (no grace). Receivers that want the §4.4 grace window use CanRekeyWithGrace.
func (*Session) CanRekeyWithGrace ¶ added in v0.4.0
CanRekeyWithGrace is the tolerance-aware variant of CanRekey. grace is the §4.4 / CONFORMANCE.md §9.3.1 receiver-side window: a session is permitted to rekey for up to grace past ExpiresAt to absorb peer-clock skew. Senders pass 0 (strict); receivers pass clockskew.Default().Grace (15 minutes).
func (*Session) EncC2S ¶
EncC2S / EncS2C / MACC2S / MACS2C expose the four directional session keys. They are used by the rekey driver to encrypt/MAC the rekey-init and rekey-accepted messages over the existing session channel. Like EnvMAC, they are raw key bytes and callers MUST NOT log, persist, or transmit them.
func (*Session) EnvMAC ¶
EnvMAC returns the K_env_mac bytes for use in seal.session_mac computation. Returns nil if the session has been erased.
This is the only public accessor for raw key material in the API. Callers MUST NOT log, persist, or transmit the returned bytes; they exist for the seal Signer to feed into the MAC primitive.
TODO(SESSION.md §5.3): consider replacing this accessor with a MAC method that performs the computation in-place, avoiding any exposure of the key bytes outside this package.
func (*Session) Erase ¶
func (s *Session) Erase()
Erase zeroes the session key material and transitions the session to StateErased. After Erase, all subsequent operations are no-ops. Callers MUST invoke Erase as part of teardown (SESSION.md §2.4, §2.6.2).
func (*Session) PreviousEnvMAC ¶ added in v0.3.0
PreviousEnvMAC returns the retained K_env_mac from before the most recent rekey. Valid only during the transition window. Returns nil outside the window or before any rekey.
func (*Session) Resumption ¶ added in v0.4.0
Resumption returns K_resumption, the secret used to derive a resumed session per HANDSHAKE.md §2.8.3. Returns nil when the session was established without resumption support.
Clients pass this value into handshake.Client.LoadResumptionSecret before invoking Resume. The value is sensitive (a leak plus observation of the resumption DH lets an attacker derive the resumed session keys); callers MUST NOT log or persist it outside the documented client-side ticket-paired storage.
func (*Session) SetKeys ¶
func (s *Session) SetKeys(k *crypto.SessionKeys)
SetKeys installs the derived session keys. Called by the handshake state machine after the shared secret is derived. The keys are owned by the session from this point on; the handshake MUST NOT retain a copy.
type State ¶
type State int
State represents a SEMP session's current lifecycle phase.
const ( // StateInitial - created but not yet established. StateInitial State = iota // StateHandshaking - handshake in progress. StateHandshaking // StateActive - handshake complete, accepting envelopes. StateActive // StateRekeying - in-session rekey exchange in progress. StateRekeying // StateExpired - TTL elapsed; key material still in memory until Erase. StateExpired // StateInvalidated - explicitly invalidated by the server (block, key // revocation, security event). StateInvalidated // StateErased - Erase has been called; the session struct is now inert. StateErased )
Lifecycle states.
type StatelessTicketIssuer ¶ added in v0.4.0
type StatelessTicketIssuer struct {
// contains filtered or unexported fields
}
StatelessTicketIssuer is a self-contained ticket implementation per SESSION.md §2.7 second bullet. The ticket value is an AEAD encryption of {identity, K_resumption, expires_at} under a server-held ticket-encryption key, prefixed with a random ticket-id used to key the consumed-ticket cache.
Wire format of one ticket (raw bytes, before base64 in the wire message):
|--- 16 bytes ---|--- 12 bytes ---|----------- ciphertext+tag -----------| | ticket_id | aead_nonce | AEAD(payload, AAD=ticket_id) |
payload is JSON: {"identity":"...","resumption":"<base64 32B>","expires_at":"<RFC3339>"}. AEAD is ChaCha20-Poly1305 with a 12-byte random nonce per ticket. Birthday collision on the nonce occurs at ~2^48 tickets per key; callers MUST rotate TicketKey at least quarterly per SESSION.md §2.7.
func NewStatelessTicketIssuer ¶ added in v0.4.0
func NewStatelessTicketIssuer(aead crypto.AEAD, ticketKey []byte) (*StatelessTicketIssuer, error)
NewStatelessTicketIssuer constructs a stateless ticket issuer backed by aead and ticketKey. ticketKey MUST be 32 bytes from a CSPRNG and SHOULD be rotated at least quarterly per SESSION.md §2.7. The returned issuer is safe for concurrent use.
func (*StatelessTicketIssuer) Consume ¶ added in v0.4.0
func (s *StatelessTicketIssuer) Consume(_ context.Context, ticket []byte) error
Consume marks the ticket's identifier as consumed in the in-memory cache. The cache entry is retained until the ticket's expires_at passes; the periodic sweep in pruneConsumed removes stale entries.
func (*StatelessTicketIssuer) Issue ¶ added in v0.4.0
func (s *StatelessTicketIssuer) Issue(_ context.Context, identity string, resumptionSecret []byte, expiresAt time.Time) ([]byte, error)
Issue implements TicketIssuer.
func (*StatelessTicketIssuer) Open ¶ added in v0.4.0
func (s *StatelessTicketIssuer) Open(_ context.Context, ticket []byte, now time.Time) (string, []byte, time.Time, error)
Open implements TicketIssuer. Returns the bound identity, resumption secret, and expires_at on success; ErrTicketUnknown, ErrTicketExpired, or ErrTicketConsumed on failure.
func (*StatelessTicketIssuer) PruneConsumed ¶ added in v0.4.0
func (s *StatelessTicketIssuer) PruneConsumed(now time.Time)
PruneConsumed removes consumed-ticket cache entries whose recorded expires_at has passed. Operators SHOULD call this periodically to bound memory.
type TicketIssuer ¶ added in v0.4.0
type TicketIssuer interface {
// Issue produces an opaque ticket binding identity and
// resumptionSecret to the given expiry. expiresAt is clamped to
// at most MaxTicketLifetime in the future.
Issue(ctx context.Context, identity string, resumptionSecret []byte, expiresAt time.Time) ([]byte, error)
// Open recovers the bound identity, resumption secret, and
// expires_at from a ticket. Returns an error if the ticket is
// corrupt, expired, or already consumed (ErrTicketUnknown,
// ErrTicketExpired, or ErrTicketConsumed).
Open(ctx context.Context, ticket []byte, now time.Time) (identity string, resumptionSecret []byte, expiresAt time.Time, err error)
// Consume marks a ticket as consumed so it cannot be reused.
// Single-use enforcement per SESSION.md §2.7.
Consume(ctx context.Context, ticket []byte) error
}
TicketIssuer issues, opens, and consumes opaque resumption tickets per SESSION.md §2.7 and HANDSHAKE.md §2.8. Implementations may be stateful (server-held table of ticket-id -> ticket payload) or stateless (AEAD-wrapped self-contained ticket bytes). The wire format is opaque to the client; only the issuing server can open a ticket it produced.
All methods are safe for concurrent use.