1use ed25519_dalek::{Signature, VerifyingKey};
4use std::collections::BTreeSet;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::ServerError;
8use crate::config::PassConfig;
9
10pub const PASS_VERSION_V1: u8 = 1;
11const SIGNATURE_LEN: usize = 64;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct PassPrincipal {
15 pub participant: Vec<u8>,
16 pub public_key: [u8; 32],
17 pub conversations: BTreeSet<u64>,
18 pub live: String,
19 pub may_enroll: bool,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct WirePassV1 {
24 pub participant: Vec<u8>,
25 pub public_key: [u8; 32],
26 pub conversations: Vec<u64>,
27 pub live: String,
28 pub may_enroll: bool,
29 pub issued_at: u64,
30 pub expires_at: u64,
31 pub signature: [u8; SIGNATURE_LEN],
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum PassCheckError {
36 Malformed,
37 Signature,
38 Expired,
39}
40impl PassCheckError {
41 #[must_use]
42 pub const fn check_name(self) -> &'static str {
43 match self {
44 Self::Malformed => "malformed",
45 Self::Signature => "signature",
46 Self::Expired => "expired",
47 }
48 }
49}
50
51#[derive(Clone, Debug)]
52pub(crate) struct PassVerifier {
53 key: VerifyingKey,
54 skew: u64,
55}
56impl PassVerifier {
57 pub(crate) fn from_config(config: &PassConfig) -> Result<Self, ServerError> {
63 let key = hex::decode(&config.registry_verifying_key).map_err(|error| {
64 ServerError::ConfigValidation {
65 message: format!(
66 "auth.pass.registry_verifying_key: expected 64 hexadecimal digits: {error}"
67 ),
68 }
69 })?;
70 Self::new(&key, config.maximum_clock_skew_seconds).map_err(|_| {
71 ServerError::ConfigValidation {
72 message: "auth.pass.registry_verifying_key: expected a valid 32-byte Ed25519 verifying key".to_owned(),
73 }
74 })
75 }
76
77 pub(crate) fn new(key: &[u8], skew: u64) -> Result<Self, PassCheckError> {
82 let key: [u8; 32] = key.try_into().map_err(|_| PassCheckError::Malformed)?;
83 let key = VerifyingKey::from_bytes(&key).map_err(|_| PassCheckError::Malformed)?;
84 Ok(Self { key, skew })
85 }
86
87 pub(crate) fn verify(&self, bytes: &[u8]) -> Result<PassPrincipal, PassCheckError> {
88 WirePassV1::verify(bytes, &self.key, self.skew, SystemTime::now())
89 }
90}
91
92impl WirePassV1 {
93 pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, PassCheckError> {
98 if self.issued_at > self.expires_at || self.conversations.windows(2).any(|p| p[0] >= p[1]) {
99 return Err(PassCheckError::Malformed);
100 }
101 let mut out = vec![PASS_VERSION_V1];
102 put_bytes(&mut out, &self.participant)?;
103 out.extend_from_slice(&self.public_key);
104 put_len(&mut out, self.conversations.len())?;
105 for id in &self.conversations {
106 out.extend_from_slice(&id.to_be_bytes());
107 }
108 put_bytes(&mut out, self.live.as_bytes())?;
109 out.push(u8::from(self.may_enroll));
110 out.extend_from_slice(&self.issued_at.to_be_bytes());
111 out.extend_from_slice(&self.expires_at.to_be_bytes());
112 Ok(out)
113 }
114
115 pub fn canonical_bytes(&self) -> Result<Vec<u8>, PassCheckError> {
120 let mut out = self.canonical_unsigned_bytes()?;
121 out.extend_from_slice(&self.signature);
122 Ok(out)
123 }
124
125 pub fn parse(bytes: &[u8]) -> Result<Self, PassCheckError> {
130 let unsigned_len = bytes
131 .len()
132 .checked_sub(SIGNATURE_LEN)
133 .ok_or(PassCheckError::Malformed)?;
134 let (unsigned, sig) = bytes.split_at(unsigned_len);
135 let mut c = Cursor(unsigned);
136 if c.u8()? != PASS_VERSION_V1 {
137 return Err(PassCheckError::Malformed);
138 }
139 let participant = c.bytes()?.to_vec();
140 let public_key = c.array()?;
141 let count = c.len()?;
142 let mut conversations = Vec::with_capacity(count);
143 for _ in 0..count {
144 conversations.push(c.u64()?);
145 }
146 let live = std::str::from_utf8(c.bytes()?)
147 .map_err(|_| PassCheckError::Malformed)?
148 .to_owned();
149 let may_enroll = match c.u8()? {
150 0 => false,
151 1 => true,
152 _ => return Err(PassCheckError::Malformed),
153 };
154 let issued_at = c.u64()?;
155 let expires_at = c.u64()?;
156 if !c.0.is_empty()
157 || issued_at > expires_at
158 || conversations.windows(2).any(|p| p[0] >= p[1])
159 {
160 return Err(PassCheckError::Malformed);
161 }
162 Ok(Self {
163 participant,
164 public_key,
165 conversations,
166 live,
167 may_enroll,
168 issued_at,
169 expires_at,
170 signature: sig.try_into().map_err(|_| PassCheckError::Malformed)?,
171 })
172 }
173
174 pub fn verify(
179 bytes: &[u8],
180 key: &VerifyingKey,
181 skew: u64,
182 now: SystemTime,
183 ) -> Result<PassPrincipal, PassCheckError> {
184 let pass = Self::parse(bytes)?;
185 key.verify_strict(
186 &pass.canonical_unsigned_bytes()?,
187 &Signature::from_bytes(&pass.signature),
188 )
189 .map_err(|_| PassCheckError::Signature)?;
190 let now = now
191 .duration_since(UNIX_EPOCH)
192 .map_err(|_| PassCheckError::Expired)?
193 .as_secs();
194 if now < pass.issued_at.saturating_sub(skew) || now > pass.expires_at.saturating_add(skew) {
195 return Err(PassCheckError::Expired);
196 }
197 Ok(PassPrincipal {
198 participant: pass.participant,
199 public_key: pass.public_key,
200 conversations: pass.conversations.into_iter().collect(),
201 live: pass.live,
202 may_enroll: pass.may_enroll,
203 })
204 }
205}
206
207fn put_len(out: &mut Vec<u8>, len: usize) -> Result<(), PassCheckError> {
208 out.extend_from_slice(
209 &u32::try_from(len)
210 .map_err(|_| PassCheckError::Malformed)?
211 .to_be_bytes(),
212 );
213 Ok(())
214}
215fn put_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), PassCheckError> {
216 put_len(out, bytes.len())?;
217 out.extend_from_slice(bytes);
218 Ok(())
219}
220struct Cursor<'a>(&'a [u8]);
221impl<'a> Cursor<'a> {
222 const fn take(&mut self, n: usize) -> Result<&'a [u8], PassCheckError> {
223 if self.0.len() < n {
224 return Err(PassCheckError::Malformed);
225 }
226 let (a, b) = self.0.split_at(n);
227 self.0 = b;
228 Ok(a)
229 }
230 fn u8(&mut self) -> Result<u8, PassCheckError> {
231 Ok(self.take(1)?[0])
232 }
233 fn len(&mut self) -> Result<usize, PassCheckError> {
234 let a: [u8; 4] = self
235 .take(4)?
236 .try_into()
237 .map_err(|_| PassCheckError::Malformed)?;
238 usize::try_from(u32::from_be_bytes(a)).map_err(|_| PassCheckError::Malformed)
239 }
240 fn u64(&mut self) -> Result<u64, PassCheckError> {
241 Ok(u64::from_be_bytes(
242 self.take(8)?
243 .try_into()
244 .map_err(|_| PassCheckError::Malformed)?,
245 ))
246 }
247 fn bytes(&mut self) -> Result<&'a [u8], PassCheckError> {
248 let n = self.len()?;
249 self.take(n)
250 }
251 fn array<const N: usize>(&mut self) -> Result<[u8; N], PassCheckError> {
252 self.take(N)?
253 .try_into()
254 .map_err(|_| PassCheckError::Malformed)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn shared_version_one_vector_is_byte_exact_and_verifies()
264 -> Result<(), Box<dyn std::error::Error>> {
265 let vector: serde_json::Value =
266 serde_json::from_str(include_str!("../test-vectors/wire-pass-v1.json"))?;
267 let unsigned = hex::decode(
268 vector["unsigned_pass_hex"]
269 .as_str()
270 .ok_or("unsigned bytes")?,
271 )?;
272 let signature: [u8; 64] =
273 hex::decode(vector["signature_hex"].as_str().ok_or("signature")?)?
274 .try_into()
275 .map_err(|_| "signature length")?;
276 let bytes = hex::decode(vector["pass_hex"].as_str().ok_or("pass bytes")?)?;
277 let registry_key: [u8; 32] = hex::decode(
278 vector["registry_verifying_key_hex"]
279 .as_str()
280 .ok_or("verifying key")?,
281 )?
282 .try_into()
283 .map_err(|_| "key length")?;
284 let pass = WirePassV1::parse(&bytes).map_err(|error| format!("parse failed: {error:?}"))?;
285 assert_eq!(
286 pass.canonical_unsigned_bytes()
287 .map_err(|error| format!("encode failed: {error:?}"))?,
288 unsigned
289 );
290 assert_eq!(pass.signature, signature);
291 let key = VerifyingKey::from_bytes(®istry_key)?;
292 let principal = WirePassV1::verify(
293 &bytes,
294 &key,
295 0,
296 UNIX_EPOCH + std::time::Duration::from_secs(pass.issued_at),
297 )
298 .map_err(|error| format!("verify failed: {error:?}"))?;
299 assert_eq!(principal.participant, b"participant-42");
300 assert_eq!(
301 principal.conversations.into_iter().collect::<Vec<_>>(),
302 vec![7, 42, 9001]
303 );
304 assert_eq!(principal.live, "workspace/acme/");
305 assert!(principal.may_enroll);
306 Ok(())
307 }
308}