zerodds_security_pki/
auth_request.rs1use alloc::format;
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24
25use crate::identity::PkiError;
26
27pub const AUTH_REQUEST_CLASS_ID: &str = "DDS:Auth:PKI-DH:1.0+AuthReq";
29
30pub const FUTURE_CHALLENGE_KEY: &str = "future_challenge";
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct AuthRequestToken {
36 pub future_challenge: [u8; 32],
39}
40
41impl AuthRequestToken {
42 #[must_use]
44 pub fn new(challenge: [u8; 32]) -> Self {
45 Self {
46 future_challenge: challenge,
47 }
48 }
49
50 #[must_use]
54 pub fn encode(&self) -> Vec<u8> {
55 let class_id = AUTH_REQUEST_CLASS_ID.as_bytes();
56 let key = FUTURE_CHALLENGE_KEY.as_bytes();
57 let mut out = Vec::with_capacity(1 + class_id.len() + 1 + key.len() + 2 + 32);
58 out.push(class_id.len() as u8);
59 out.extend_from_slice(class_id);
60 out.push(key.len() as u8);
61 out.extend_from_slice(key);
62 out.extend_from_slice(&(32u16.to_be_bytes()));
63 out.extend_from_slice(&self.future_challenge);
64 out
65 }
66
67 pub fn decode(bytes: &[u8]) -> Result<Self, PkiError> {
76 let mut pos = 0usize;
77 if bytes.len() <= pos {
78 return Err(PkiError::InvalidPem("AuthReq truncated at class-id".into()));
79 }
80 let cid_len = bytes[pos] as usize;
81 pos += 1;
82 if bytes.len() < pos + cid_len {
83 return Err(PkiError::InvalidPem("AuthReq class-id truncated".into()));
84 }
85 let cid = core::str::from_utf8(&bytes[pos..pos + cid_len])
86 .map_err(|_| PkiError::InvalidPem("AuthReq class-id non-utf8".into()))?;
87 if cid != AUTH_REQUEST_CLASS_ID {
88 return Err(PkiError::InvalidPem(format!(
89 "AuthReq class-id mismatch: got `{cid}`"
90 )));
91 }
92 pos += cid_len;
93 if bytes.len() <= pos {
94 return Err(PkiError::InvalidPem(
95 "AuthReq truncated at key-length".into(),
96 ));
97 }
98 let key_len = bytes[pos] as usize;
99 pos += 1;
100 if bytes.len() < pos + key_len {
101 return Err(PkiError::InvalidPem("AuthReq key truncated".into()));
102 }
103 let key = core::str::from_utf8(&bytes[pos..pos + key_len])
104 .map_err(|_| PkiError::InvalidPem("AuthReq key non-utf8".into()))?;
105 if key != FUTURE_CHALLENGE_KEY {
106 return Err(PkiError::InvalidPem(format!(
107 "AuthReq key mismatch: got `{key}`"
108 )));
109 }
110 pos += key_len;
111 if bytes.len() < pos + 2 {
112 return Err(PkiError::InvalidPem(
113 "AuthReq value-length truncated".into(),
114 ));
115 }
116 let val_len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
117 pos += 2;
118 if val_len != 32 {
119 return Err(PkiError::InvalidPem(format!(
120 "AuthReq future_challenge must be 32 bytes, got {val_len}"
121 )));
122 }
123 if bytes.len() < pos + val_len {
124 return Err(PkiError::InvalidPem("AuthReq value truncated".into()));
125 }
126 let mut challenge = [0u8; 32];
127 challenge.copy_from_slice(&bytes[pos..pos + 32]);
128 Ok(Self {
129 future_challenge: challenge,
130 })
131 }
132}
133
134#[must_use]
137pub fn auth_request_properties(token: &AuthRequestToken) -> Vec<(String, Vec<u8>)> {
138 alloc::vec![(
139 FUTURE_CHALLENGE_KEY.to_string(),
140 token.future_challenge.to_vec(),
141 )]
142}
143
144#[cfg(test)]
145#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn round_trip_preserves_challenge() {
151 let chal = [0x42u8; 32];
152 let token = AuthRequestToken::new(chal);
153 let bytes = token.encode();
154 let back = AuthRequestToken::decode(&bytes).unwrap();
155 assert_eq!(back, token);
156 }
157
158 #[test]
159 fn class_id_must_match() {
160 let mut bytes = AuthRequestToken::new([1u8; 32]).encode();
161 bytes[5] ^= 0xff;
163 let err = AuthRequestToken::decode(&bytes).unwrap_err();
164 assert!(matches!(err, PkiError::InvalidPem(_)));
165 }
166
167 #[test]
168 fn truncated_buffer_rejected() {
169 let bytes = AuthRequestToken::new([1u8; 32]).encode();
170 for cut in 1..bytes.len() {
171 assert!(
172 AuthRequestToken::decode(&bytes[..cut]).is_err(),
173 "buffer truncated at {cut} should fail"
174 );
175 }
176 }
177
178 #[test]
179 fn wrong_value_length_rejected() {
180 let mut bytes = AuthRequestToken::new([1u8; 32]).encode();
181 let val_len_pos = 1 + AUTH_REQUEST_CLASS_ID.len() + 1 + FUTURE_CHALLENGE_KEY.len();
183 bytes[val_len_pos] = 0;
184 bytes[val_len_pos + 1] = 16;
185 let err = AuthRequestToken::decode(&bytes).unwrap_err();
186 assert!(matches!(err, PkiError::InvalidPem(_)));
187 }
188
189 #[test]
190 fn properties_helper_returns_kv_list() {
191 let token = AuthRequestToken::new([7u8; 32]);
192 let props = auth_request_properties(&token);
193 assert_eq!(props.len(), 1);
194 assert_eq!(props[0].0, FUTURE_CHALLENGE_KEY);
195 assert_eq!(props[0].1.len(), 32);
196 }
197
198 #[test]
199 fn class_id_constant_matches_spec() {
200 assert_eq!(AUTH_REQUEST_CLASS_ID, "DDS:Auth:PKI-DH:1.0+AuthReq");
202 }
203
204 #[test]
205 fn empty_input_rejected() {
206 assert!(AuthRequestToken::decode(&[]).is_err());
207 }
208
209 #[test]
210 fn class_id_length_byte_too_large_rejected() {
211 let bytes = alloc::vec![0xff, b'x'];
212 assert!(AuthRequestToken::decode(&bytes).is_err());
213 }
214}