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
impl Session
Sourcepub fn new() -> Self
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.
Sourcepub fn with_source_id(source_id: [u8; 8], epoch: u8) -> Self
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.
Sourcepub fn with_rekey_grace(self, grace: Duration) -> Self
pub fn with_rekey_grace(self, grace: Duration) -> Self
Override the default rekey grace period. Must be called before the first rekey.
Sourcepub fn install_key(&mut self, key: [u8; 32])
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.
Sourcepub fn tick(&mut self)
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.
Sourcepub fn next_nonce(&mut self) -> u64
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.
Sourcepub fn nonce_counter(&self) -> u64
pub fn nonce_counter(&self) -> u64
Current AEAD nonce counter, for observability.
Sourcepub fn key_established_at(&self) -> Option<Instant>
pub fn key_established_at(&self) -> Option<Instant>
Time the current key was installed, if any.
Sourcepub fn consent_state(&self) -> ConsentState
Available on crate feature consent only.
pub fn consent_state(&self) -> ConsentState
consent only.Current consent ceremony state (SPEC draft-03 §12).
Only available when the consent feature is enabled.
Sourcepub fn session_fingerprint(
&self,
request_id: u64,
) -> Result<[u8; 32], WireError>
Available on crate feature consent only.
pub fn session_fingerprint( &self, request_id: u64, ) -> Result<[u8; 32], WireError>
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 bytesBoth 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).
Sourcepub fn sign_consent_request(
&self,
core: ConsentRequestCore,
signing_key: &SigningKey,
) -> Result<ConsentRequest, WireError>
Available on crate feature consent only.
pub fn sign_consent_request( &self, core: ConsentRequestCore, signing_key: &SigningKey, ) -> Result<ConsentRequest, WireError>
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.
Sourcepub fn sign_consent_response(
&self,
core: ConsentResponseCore,
signing_key: &SigningKey,
) -> Result<ConsentResponse, WireError>
Available on crate feature consent only.
pub fn sign_consent_response( &self, core: ConsentResponseCore, signing_key: &SigningKey, ) -> Result<ConsentResponse, WireError>
consent only.Sign a [ConsentResponseCore] after injecting the session
fingerprint for the core’s request_id. See
Self::sign_consent_request.
Sourcepub fn sign_consent_revocation(
&self,
core: ConsentRevocationCore,
signing_key: &SigningKey,
) -> Result<ConsentRevocation, WireError>
Available on crate feature consent only.
pub fn sign_consent_revocation( &self, core: ConsentRevocationCore, signing_key: &SigningKey, ) -> Result<ConsentRevocation, WireError>
consent only.Sign a [ConsentRevocationCore] after injecting the session
fingerprint for the core’s request_id. See
Self::sign_consent_request.
Sourcepub fn verify_consent_request(
&self,
req: &ConsentRequest,
expected_pubkey: Option<&[u8; 32]>,
) -> bool
Available on crate feature consent only.
pub fn verify_consent_request( &self, req: &ConsentRequest, expected_pubkey: Option<&[u8; 32]>, ) -> bool
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:
- The Ed25519 signature is valid,
- The embedded public key matches
expected_pubkey(if provided), and - The embedded
session_fingerprintmatches 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.
Sourcepub fn verify_consent_response(
&self,
resp: &ConsentResponse,
expected_pubkey: Option<&[u8; 32]>,
) -> bool
Available on crate feature consent only.
pub fn verify_consent_response( &self, resp: &ConsentResponse, expected_pubkey: Option<&[u8; 32]>, ) -> bool
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.
Sourcepub fn verify_consent_revocation(
&self,
rev: &ConsentRevocation,
expected_pubkey: Option<&[u8; 32]>,
) -> bool
Available on crate feature consent only.
pub fn verify_consent_revocation( &self, rev: &ConsentRevocation, expected_pubkey: Option<&[u8; 32]>, ) -> bool
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.
Sourcepub fn observe_consent(
&mut self,
event: ConsentEvent,
) -> Result<ConsentState, ConsentViolation>
Available on crate feature consent only.
pub fn observe_consent( &mut self, event: ConsentEvent, ) -> Result<ConsentState, ConsentViolation>
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.
| Current | Event | Next state / action |
|---|---|---|
AwaitingRequest | Request{id} | → Requested, active_id = id |
AwaitingRequest | Response{*, id} | → StaleResponseForUnknownRequest |
AwaitingRequest | Revocation{id} | → RevocationBeforeApproval |
Requested | Request{id}, id > active | → Requested, active_id = id (replacement) |
Requested | Request{id}, id ≤ active | no-op (stale) |
Requested | ResponseApproved{id==active} | → Approved, record last_response=true |
Requested | ResponseDenied{id==active} | → Denied, record last_response=false |
Requested | Response{id≠active} | → StaleResponseForUnknownRequest |
Requested | Revocation{id} | → RevocationBeforeApproval |
Approved | Request{id}, id > active | → Requested, reset tracking (new ceremony) |
Approved | ResponseApproved{id==active} | no-op (idempotent) |
Approved | ResponseDenied{id==active} | → ContradictoryResponse{prior=true, new=false} |
Approved | Response{id≠active} | → StaleResponseForUnknownRequest |
Approved | Revocation{id==active} | → Revoked |
Approved | Revocation{id≠active} | no-op (stale revocation) |
Denied | Request{id}, id > active | → Requested, reset tracking (new ceremony) |
Denied | ResponseDenied{id==active} | no-op (idempotent) |
Denied | ResponseApproved{id==active} | → ContradictoryResponse{prior=false, new=true} |
Denied | Revocation{id} | no-op (nothing to revoke) |
Revoked | Request{id}, id > active | → Requested, 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.
Sourcepub fn seal(
&mut self,
plaintext: &[u8],
payload_type: u8,
) -> Result<Vec<u8>, WireError>
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).
Sourcepub fn open(&mut self, envelope: &[u8]) -> Result<Vec<u8>, WireError>
pub fn open(&mut self, envelope: &[u8]) -> Result<Vec<u8>, WireError>
Open a sealed envelope and return the plaintext.
Performs three checks in order:
- Length: envelope must be at least 28 bytes (12 nonce + 16 tag).
- AEAD verify: ChaCha20-Poly1305 decrypt against the current session key, falling back to the previous key during the rekey grace period.
- 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
impl Session
Sourcepub fn builder() -> SessionBuilder
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.