pub struct CaseResponder { /* private fields */ }Expand description
Responder-side CASE state machine (new-session path).
Handles the Sigma1 / Sigma2 / Sigma3 handshake from the responder’s
(device’s) perspective. Sans-IO: the caller feeds raw bytes in via
handle_sigma1 and
handle_sigma3, and reads raw bytes out via
next_message.
§Construction
CaseResponder::new— production constructor; uses the OS CSPRNG.new_using_rng(crate-internal) — deterministic constructor for tests; accepts an injectablering::rand::SecureRandom.
§Driving the handshake
- Receive Sigma1 bytes from the peer.
- Call
handle_sigma1with those bytes.- Returns
Sigma1Outcome::NewSessionfor a fresh session (M4.1). - Returns
Errif thedest_iddoesn’t match our fabric identity.
- Returns
- Call
next_message→ get Sigma2 bytes; send them. - Receive Sigma3 bytes from the peer.
- Call
handle_sigma3with those bytes. - Send a
StatusReport: Successto the initiator. - Call
finishto retrieveCaseSessionOutput.
Use expected_inbound at any point to query
which message the machine is currently waiting to receive.
Implementations§
Source§impl CaseResponder
impl CaseResponder
Sourcepub fn new(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
responder_session_id: u16,
now: MatterTime,
) -> Result<Self>
pub fn new( credentials: CaseCredentials, trusted_roots: TrustedRoots, responder_session_id: u16, now: MatterTime, ) -> Result<Self>
Construct a responder using the OS CSPRNG.
Pre-samples the ephemeral keypair and 32-byte responder random so that
handle_sigma1 cannot fail due to randomness.
responder_session_id is the non-zero secured-session id this responder
advertises in Sigma2 (tag 2) for the peer to address us by; it is
recorded as CaseSessionOutput.local.session_id once the handshake
completes.
now is the wall-clock instant against which the initiator’s
operational certificate chain is checked for temporal validity during
Sigma3. This crate never reads the system clock; the caller (controller
layer) must supply the real time.
§Errors
Returns Error::EphemeralKeyGenerationFailed if the OS RNG fails
(extremely unlikely in practice).
Sourcepub fn expected_inbound(&self) -> Option<CaseMessageKind>
pub fn expected_inbound(&self) -> Option<CaseMessageKind>
Returns the CASE message kind the machine is currently waiting to
receive, or None if the machine is in an outbound-only state,
has completed, or has been poisoned.
Sourcepub fn handle_sigma1(&mut self, bytes: &[u8]) -> Result<Sigma1Outcome>
pub fn handle_sigma1(&mut self, bytes: &[u8]) -> Result<Sigma1Outcome>
Process the inbound Sigma1 message.
Verifies that the dest_id in Sigma1 matches the responder’s fabric
identity.
New-session path: If Sigma1 carries no resumption fields, computes
the ECDH shared secret, builds and encrypts TBEData2, signs TBSData2
with our NOC key, encodes the Sigma2 message, advances to
ReadyToSendSigma2, and returns Sigma1Outcome::NewSession.
Resumption path: If Sigma1 carries both resumption_id (tag 6) and
initiator_resume_mic (tag 7), transitions to AwaitingResumptionDecision
and returns Sigma1Outcome::ResumptionRequested. The caller must then
look up the ResumptionRecord and call either
accept_resumption or
reject_resumption.
§Errors
Error::UnexpectedCaseMessageif called from the wrong state.Error::InvalidParameterif thedest_idin Sigma1 does not match our fabric identity, or TLV decode fails.Error::EphemeralKeyGenerationFailedif ECDH or HKDF fails.Error::SigningFailedif our NOC signing step fails.Error::Codecon TLV encoding failure.
Sourcepub fn accept_resumption(&mut self, record: ResumptionRecord) -> Result<()>
pub fn accept_resumption(&mut self, record: ResumptionRecord) -> Result<()>
Accept a resumption attempt: verify the initiator’s MIC, derive session
keys, build the Sigma2_Resume message, and advance to
ReadyToSendSigma2Resume.
Must be called after handle_sigma1 returns
Sigma1Outcome::ResumptionRequested with the caller-supplied
ResumptionRecord that matches id in the outcome.
§Resumption session-key layout
Pinned from matter.js NodeSession.create (isResumption = true branch,
responder isInitiator = false):
keys = HKDF(ikm = shared_secret,
salt = initiatorRandom || OLD_resumption_id,
info = "SessionResumptionKeys",
len = 48)
// Responder (isInitiator=false) key assignment:
keys[0..16] → r2i_key (responder encrypts to initiator)
keys[16..32] → i2r_key (responder decrypts from initiator)
keys[32..48] → attestation_challengeThis layout is the same byte positions as the initiator uses, but the
semantic labels align with the responder’s direction (see matter.js
NodeSession.ts isInitiator=false branch).
§Errors
Error::UnexpectedCaseMessageif called from the wrong state.Error::InvalidParameterifrecord.iddoes not match theresumption_idthe initiator presented.Error::ResumptionMacMismatchif theinitiator_resume_micin Sigma1 does not verify againstrecord.shared_secret.Error::EphemeralKeyGenerationFailedif the OS RNG or HKDF fails.Error::Codecon TLV encoding failure.
Sourcepub fn reject_resumption(&mut self) -> Result<()>
pub fn reject_resumption(&mut self) -> Result<()>
Decline a resumption attempt and fall back to the new-session path.
Must be called after handle_sigma1 returns
Sigma1Outcome::ResumptionRequested. After this call, the state
machine is in the same state as it would be after a regular Sigma1
(new-session path). The next call to next_message
will return Sigma2 bytes (not Sigma2_Resume).
§Errors
Error::UnexpectedCaseMessageif called from the wrong state.
Sourcepub fn next_message(&mut self) -> Result<Vec<u8>>
pub fn next_message(&mut self) -> Result<Vec<u8>>
Retrieve the next outbound message and advance the state machine.
New-session path: Returns the Sigma2 bytes and advances to
AwaitingSigma3. Must be called after a successful
handle_sigma1 that returned
Sigma1Outcome::NewSession, or after
reject_resumption.
Resumption path: Returns the Sigma2_Resume bytes and advances
directly to Complete. Must be called after a successful
accept_resumption. There is no
Sigma3_Resume — the handshake completes after Sigma2_Resume is sent
(confirmed from matter.js).
§Errors
Error::UnexpectedCaseMessageif called from the wrong state.
Sourcepub fn handle_sigma3(&mut self, bytes: &[u8]) -> Result<()>
pub fn handle_sigma3(&mut self, bytes: &[u8]) -> Result<()>
Process the inbound Sigma3 message, verify the initiator’s credentials, and derive the final session keys.
§Sigma3 processing steps
- Derive S3K via HKDF (same salt construction as initiator, mirrored).
- AES-128-CCM decrypt the encrypted blob using S3K and the
NCASE_Sigma3Nnonce. - Parse
TBEData3={ initiatorNoc, initiatorIcac?, signature }. - Validate the initiator’s NOC chain against
trusted_roots. - Extract initiator
NodeId+FabricIdfrom NOC subject. - Verify
FabricIdmatches our credentials. - Verify the initiator’s ECDSA signature over
TBSData3. - Derive final session keys; assign i2r/r2i with responder convention.
§Key assignment convention (responder, isInitiator=false in matter.js)
decryptKey = keys[0..16] (responder decrypts initiator traffic = i2r)
encryptKey = keys[16..32] (responder encrypts to initiator = r2i)
attestationChallenge = keys[32..48]Pinned from NodeSession.ts, isInitiator=false branch.
§Errors
Error::UnexpectedCaseMessageif called from the wrong state.Error::EphemeralKeyGenerationFailedif HKDF fails.Error::EncryptedBlobDecryptionFailedif the encrypted blob fails AEAD verification.Error::Codec/Error::InvalidParameteron TLV decode failure.Error::InvalidPeerNocChainif chain validation fails.Error::FabricIdMismatchif the initiator’s NOC carries a differentFabricIdthan our credentials.Error::PeerSignatureInvalidif the initiator’s ECDSA signature fails.
Sourcepub fn finish(self) -> Result<CaseSessionOutput>
pub fn finish(self) -> Result<CaseSessionOutput>
Finalise the session and retrieve the derived CaseSessionOutput.
May only be called after handle_sigma3 has
completed (i.e., the state machine is in the Complete state).
§Errors
Error::HandshakeIncompleteif called before all handshake phases have completed.