Skip to main content

liminal_protocol/
credential.rs

1//! The credential records a `Connect` may carry in its authentication bytes.
2//!
3//! ACCESS-001 R2: a Lys pass is admitted as a second credential kind beside
4//! `WirePassV1`, as a new record and never as trailing fields on V1. The two
5//! records are told apart by their first byte; everything after the Lys
6//! record's tag is the pass exactly as Lys issued it, so the server hands it
7//! to `lys-pass` unchanged and never parses it here.
8//!
9//! The bearer token the server may also accept is not a record: it has no
10//! tag and is compared whole, before any record is read. Classification runs
11//! only on bytes the bearer comparison refused.
12
13use alloc::vec::Vec;
14
15/// First byte of a `WirePassV1` record, owned by the server's pass checker.
16pub const WIRE_PASS_V1_TAG: u8 = 1;
17/// First byte of a Lys pass record; the rest is the pass as Lys issued it.
18pub const LYS_PASS_V1_TAG: u8 = 2;
19
20/// A credential record classified by its first byte.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum WireCredential<'a> {
23    /// A `WirePassV1` record; the whole bytes, tag included, go to its checker.
24    PassV1(&'a [u8]),
25    /// A Lys pass; the bytes after the tag go to `lys-pass` unchanged.
26    LysPass(&'a [u8]),
27}
28
29/// Why bytes are not a credential record.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum CredentialKindError {
32    /// No bytes at all.
33    Empty,
34    /// A first byte no record kind claims.
35    UnknownTag(u8),
36    /// A Lys record tag with nothing after it.
37    EmptyLysPass,
38}
39
40impl core::fmt::Display for CredentialKindError {
41    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match self {
43            Self::Empty => formatter.write_str("credential is empty"),
44            Self::UnknownTag(tag) => write!(formatter, "credential tag {tag} is unknown"),
45            Self::EmptyLysPass => formatter.write_str("lys pass record carries no pass"),
46        }
47    }
48}
49
50impl core::error::Error for CredentialKindError {}
51
52impl<'a> WireCredential<'a> {
53    /// Classifies credential bytes by their first byte.
54    ///
55    /// # Errors
56    /// Empty bytes, an unknown tag, or a Lys tag with no pass behind it.
57    pub const fn classify(bytes: &'a [u8]) -> Result<Self, CredentialKindError> {
58        match bytes {
59            [] => Err(CredentialKindError::Empty),
60            [WIRE_PASS_V1_TAG, ..] => Ok(Self::PassV1(bytes)),
61            [LYS_PASS_V1_TAG] => Err(CredentialKindError::EmptyLysPass),
62            [LYS_PASS_V1_TAG, pass @ ..] => Ok(Self::LysPass(pass)),
63            [tag, ..] => Err(CredentialKindError::UnknownTag(*tag)),
64        }
65    }
66
67    /// Frames a Lys pass as the record a client sends.
68    #[must_use]
69    pub fn lys_record(pass: &[u8]) -> Vec<u8> {
70        let mut record = Vec::with_capacity(pass.len() + 1);
71        record.push(LYS_PASS_V1_TAG);
72        record.extend_from_slice(pass);
73        record
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{CredentialKindError, LYS_PASS_V1_TAG, WIRE_PASS_V1_TAG, WireCredential};
80
81    #[test]
82    fn a_v1_record_keeps_its_tag_for_the_pass_checker() {
83        let bytes = [WIRE_PASS_V1_TAG, 7, 8];
84        assert_eq!(
85            WireCredential::classify(&bytes),
86            Ok(WireCredential::PassV1(&bytes))
87        );
88    }
89
90    #[test]
91    fn a_lys_record_hands_over_the_pass_without_its_tag() {
92        let record = WireCredential::lys_record(b"issued-by-lys");
93        assert_eq!(record[0], LYS_PASS_V1_TAG);
94        assert_eq!(
95            WireCredential::classify(&record),
96            Ok(WireCredential::LysPass(b"issued-by-lys"))
97        );
98    }
99
100    #[test]
101    fn bytes_outside_both_records_are_refused_by_name() {
102        assert_eq!(
103            WireCredential::classify(&[]),
104            Err(CredentialKindError::Empty)
105        );
106        assert_eq!(
107            WireCredential::classify(&[LYS_PASS_V1_TAG]),
108            Err(CredentialKindError::EmptyLysPass)
109        );
110        assert_eq!(
111            WireCredential::classify(&[9, 1]),
112            Err(CredentialKindError::UnknownTag(9))
113        );
114    }
115}