Skip to main content

Session

Struct Session 

Source
pub struct Session { /* private fields */ }
Expand description

Session state for a single logical stream.

See the module-level docs for what Session owns and what it deliberately does not. Use Session::new to construct with random source_id + epoch, then Session::install_key before the first seal or open.

Implementations§

Source§

impl Session

Source

pub fn new() -> Self

Construct a session with random source_id + epoch and no key yet.

Call Self::install_key before the first seal or open.

Source

pub fn with_source_id(source_id: [u8; 8], epoch: u8) -> Self

Construct a session with caller-supplied source_id + epoch.

Primarily useful for test fixtures and deterministic replay. The caller MUST ensure no two live sessions share the same (source_id, epoch, key) tuple — nonce reuse under ChaCha20-Poly1305 catastrophically breaks confidentiality.

Source

pub fn with_rekey_grace(self, grace: Duration) -> Self

Override the default rekey grace period. Must be called before the first rekey.

Source

pub fn install_key(&mut self, key: [u8; 32])

Install a 32-byte session key.

First call installs the initial key. Subsequent calls perform a rekey: the previous key is moved to prev_session_key with a grace-period expiry of the configured rekey_grace; the new key becomes current; the nonce counter resets to zero.

The replay window is NOT cleared on rekey, but it IS scoped by key epoch (SPEC §5.3 / draft-02r1) — the incoming new-key stream starts a fresh per-epoch window rather than fighting the old key’s high-water mark. When the previous key expires in Self::tick, its per-epoch replay state is dropped.

Source

pub fn has_key(&self) -> bool

Return true if the session has a current key.

Source

pub fn tick(&mut self)

Advance session state that depends on wall-clock time.

Call periodically (e.g. once per tick, or lazily before seal/open) to expire the previous key once the grace period has elapsed.

Source

pub fn next_nonce(&mut self) -> u64

Allocate the next AEAD nonce sequence number.

Uses wrapping_add to avoid a debug-build panic on overflow; the low 32 bits embedded in the nonce wrap in ~4.5 years of continuous operation at 30 fps. Real session lifetime is governed by rekey cadence, so wraparound is not a practical concern.

Source

pub fn nonce_counter(&self) -> u64

Current AEAD nonce counter, for observability.

Source

pub fn source_id(&self) -> &[u8; 8]

Per-session source identifier.

Source

pub fn epoch(&self) -> u8

Per-session epoch byte.

Source

pub fn key_established_at(&self) -> Option<Instant>

Time the current key was installed, if any.

Source

pub fn consent_state(&self) -> ConsentState

Available on crate feature consent only.

Current consent ceremony state (SPEC draft-03 §12).

Only available when the consent feature is enabled.

Source

pub fn session_fingerprint( &self, request_id: u64, ) -> Result<[u8; 32], WireError>

Available on crate feature consent only.

Derive the 32-byte session fingerprint for a given request_id (SPEC draft-03 §12.3.1).

The fingerprint is HKDF-SHA-256 over the current session key:

salt   = b"xenia-session-fingerprint-v1"
ikm    = current session_key   (32 bytes)
info   = source_id || epoch || request_id.to_be_bytes()
         (8 + 1 + 8 = 17 bytes)
output = 32 bytes

Both peers derive the same fingerprint from their own copy of the session key. Each peer embeds the derived fingerprint in every signed consent message body; receivers re-derive locally and compare. A signed consent message whose fingerprint does not match the receiver’s derivation has been replayed from a different session and MUST be rejected.

Returns WireError::NoSessionKey if no key is installed. Callers SHOULD derive the fingerprint immediately before signing / verifying and not cache it across rekeys — the fingerprint changes with the session key.

On the verify side, prefer the convenience helpers Self::verify_consent_request / _response / _revocation rather than calling this directly, because those helpers transparently probe the previous key during the rekey grace window (a consent message sealed moments before rekey can legitimately carry a prev-key fingerprint; see §12.3.1 rekey interaction).

Available on crate feature consent only.

Sign a [ConsentRequestCore] after injecting the session fingerprint derived from this session’s state and the core’s request_id (SPEC draft-03 §12.3 / §12.3.1).

The caller constructs a ConsentRequestCore with any session_fingerprint value (the helper overwrites it). On the send path this is the recommended entry point; it removes the possibility of a caller forgetting to derive-and-inject.

Available on crate feature consent only.

Sign a [ConsentResponseCore] after injecting the session fingerprint for the core’s request_id. See Self::sign_consent_request.

Available on crate feature consent only.

Sign a [ConsentRevocationCore] after injecting the session fingerprint for the core’s request_id. See Self::sign_consent_request.

Available on crate feature consent only.

Verify a [ConsentRequest] against this session’s fingerprint AND the requester’s public key (SPEC draft-03 §12.3.1).

Returns true iff:

  1. The Ed25519 signature is valid,
  2. The embedded public key matches expected_pubkey (if provided), and
  3. The embedded session_fingerprint matches the fingerprint this session derives locally under either the current session key OR the previous session key (during the rekey grace window). Probing both covers the in-flight case where the sender signed under the previous key moments before a rekey and the message arrived after the receiver rekeyed.

Returns false (never a WireError) on any mismatch — per SPEC §11 the caller should react to verification failure the same way for all sub-cases.

Available on crate feature consent only.

Verify a [ConsentResponse] against this session’s fingerprint AND the responder’s public key. See Self::verify_consent_request — same both-epochs probing behavior for the rekey grace window.

Available on crate feature consent only.

Verify a [ConsentRevocation] against this session’s fingerprint AND the revoker’s public key. See Self::verify_consent_request — same both-epochs probing behavior for the rekey grace window.

Available on crate feature consent only.

Drive the consent state machine from an observed consent message.

Callers invoke this AFTER successfully opening a consent envelope (PAYLOAD_TYPE_CONSENT_REQUEST / _RESPONSE / _REVOCATION) and verifying the signature AND the session fingerprint. The session does not validate signatures or fingerprints itself — that’s an application policy decision (which pubkeys to trust, which expiry windows to accept). Use Self::verify_consent_request (and siblings) for the standard verification path.

§Transition table (SPEC draft-03 §12.6)

LegacyBypass is sticky — every event is a no-op, state stays LegacyBypass. The caller opts into ceremony mode at construction via SessionBuilder::require_consent; a session in LegacyBypass never emits or honors consent events.

For the remaining states, id refers to event.request_id() and active refers to the session’s active_request_id.

CurrentEventNext state / action
AwaitingRequestRequest{id}Requested, active_id = id
AwaitingRequestResponse{*, id}StaleResponseForUnknownRequest
AwaitingRequestRevocation{id}RevocationBeforeApproval
RequestedRequest{id}, id > activeRequested, active_id = id (replacement)
RequestedRequest{id}, id ≤ activeno-op (stale)
RequestedResponseApproved{id==active}Approved, record last_response=true
RequestedResponseDenied{id==active}Denied, record last_response=false
RequestedResponse{id≠active}StaleResponseForUnknownRequest
RequestedRevocation{id}RevocationBeforeApproval
ApprovedRequest{id}, id > activeRequested, reset tracking (new ceremony)
ApprovedResponseApproved{id==active}no-op (idempotent)
ApprovedResponseDenied{id==active}ContradictoryResponse{prior=true, new=false}
ApprovedResponse{id≠active}StaleResponseForUnknownRequest
ApprovedRevocation{id==active}Revoked
ApprovedRevocation{id≠active}no-op (stale revocation)
DeniedRequest{id}, id > activeRequested, reset tracking (new ceremony)
DeniedResponseDenied{id==active}no-op (idempotent)
DeniedResponseApproved{id==active}ContradictoryResponse{prior=false, new=true}
DeniedRevocation{id}no-op (nothing to revoke)
RevokedRequest{id}, id > activeRequested, reset tracking (fresh ceremony)
Revoked*no-op

Bold entries return Err(ConsentViolation); the state is NOT mutated on violation (the caller is expected to tear down).

§Returns
  • Ok(state) on any legal transition or benign no-op.
  • Err(ConsentViolation) when the peer emitted an event that cannot follow the current state. The session state is left untouched. The caller SHOULD terminate the session.
Source

pub fn seal( &mut self, plaintext: &[u8], payload_type: u8, ) -> Result<Vec<u8>, WireError>

Seal a binary plaintext under the current session key using ChaCha20-Poly1305.

Wire format: [ nonce (12 bytes) | ciphertext | poly1305 tag (16 bytes) ].

Nonce layout: source_id[0..6] | payload_type | epoch | sequence[0..4] — little-endian on the sequence portion.

Returns WireError::NoSessionKey if no key is installed, or WireError::SealFailed if the underlying AEAD implementation rejects the input (should not happen with a valid 32-byte key).

Source

pub fn open(&mut self, envelope: &[u8]) -> Result<Vec<u8>, WireError>

Open a sealed envelope and return the plaintext.

Performs three checks in order:

  1. Length: envelope must be at least 28 bytes (12 nonce + 16 tag).
  2. AEAD verify: ChaCha20-Poly1305 decrypt against the current session key, falling back to the previous key during the rekey grace period.
  3. Replay window: the sequence embedded in nonce bytes 8..12 (little-endian u32) must be either strictly higher than any previously accepted sequence for the same (source_id, payload_type) stream, OR within the 64-message sliding window AND not previously seen.

Returns WireError::OpenFailed on any failure. Mutates self to advance the replay window on success.

The payload type embedded in the nonce is used for replay-window keying only — the caller is responsible for dispatching the returned plaintext to the correct deserializer.

Source§

impl Session

Source

pub fn builder() -> SessionBuilder

Start a SessionBuilder for opt-in configuration. Use this when you want require_consent(), a non-default replay window size, or deterministic fixture source_id / epoch without stacking multiple post-construction mutators.

Added in draft-02r2.

Trait Implementations§

Source§

impl Default for Session

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V