use ed25519_dalek::{Signature, VerifyingKey};
use std::collections::BTreeSet;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ServerError;
use crate::config::PassConfig;
pub const PASS_VERSION_V1: u8 = 1;
const SIGNATURE_LEN: usize = 64;
pub(crate) const MAX_PASS_CONVERSATIONS: usize =
crate::server::connection::pre_admission::PRE_ADMISSION_FRAME_BOUND / 8;
const _: () = assert!(MAX_PASS_CONVERSATIONS == 8192);
#[derive(Clone, PartialEq, Eq)]
pub struct PassPrincipal {
pub participant: Vec<u8>,
pub public_key: [u8; 32],
pub conversations: BTreeSet<u64>,
pub live: String,
pub may_enroll: bool,
}
#[derive(Clone, PartialEq, Eq)]
pub struct WirePassV1 {
pub participant: Vec<u8>,
pub public_key: [u8; 32],
pub conversations: Vec<u64>,
pub live: String,
pub may_enroll: bool,
pub issued_at: u64,
pub expires_at: u64,
pub signature: [u8; SIGNATURE_LEN],
}
pub(crate) struct ByteLength(pub(crate) usize);
impl std::fmt::Debug for ByteLength {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{} bytes", self.0)
}
}
impl std::fmt::Debug for PassPrincipal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PassPrincipal")
.field("participant", &ByteLength(self.participant.len()))
.field("public_key", &ByteLength(self.public_key.len()))
.field("conversations", &self.conversations)
.field("live", &ByteLength(self.live.len()))
.field("may_enroll", &self.may_enroll)
.finish()
}
}
impl std::fmt::Debug for WirePassV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("WirePassV1")
.field("participant", &ByteLength(self.participant.len()))
.field("public_key", &ByteLength(self.public_key.len()))
.field("conversations", &self.conversations)
.field("live", &ByteLength(self.live.len()))
.field("may_enroll", &self.may_enroll)
.field("issued_at", &self.issued_at)
.field("expires_at", &self.expires_at)
.field("signature", &ByteLength(self.signature.len()))
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct VerifiedHolder {
participant: Vec<u8>,
live: String,
source: HolderSource,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum HolderSource {
RegistryPass,
Lys { kind: Option<String> },
}
impl VerifiedHolder {
pub(crate) fn from_lys_session(session: &dyn crate::auth_lys::session::LysSession) -> Self {
Self {
participant: session.holder().as_bytes().to_vec(),
live: String::new(),
source: HolderSource::Lys {
kind: session.holder_kind().map(str::to_owned),
},
}
}
#[must_use]
pub fn kind(&self) -> Option<&str> {
match &self.source {
HolderSource::RegistryPass => None,
HolderSource::Lys { kind } => kind.as_deref(),
}
}
#[must_use]
pub fn allows_channel(&self, channel: &str) -> bool {
match self.source {
HolderSource::RegistryPass => {
!self.live.is_empty() && channel.starts_with(self.live.as_str())
}
HolderSource::Lys { .. } => true,
}
}
#[must_use]
pub fn participant(&self) -> &[u8] {
&self.participant
}
#[must_use]
pub fn live(&self) -> &str {
&self.live
}
#[must_use]
pub fn is_named(&self, identity: &str) -> bool {
identity.as_bytes() == self.participant.as_slice()
}
}
impl std::fmt::Debug for VerifiedHolder {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("VerifiedHolder")
.field("participant", &ByteLength(self.participant.len()))
.field("live", &ByteLength(self.live.len()))
.field("source", &self.source)
.finish()
}
}
pub(crate) struct VerifiedPass {
pub(crate) principal: PassPrincipal,
pub(crate) holder: VerifiedHolder,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PassCheckError {
Malformed,
ScopeTooLarge,
Signature,
Expired,
}
impl PassCheckError {
#[must_use]
pub const fn check_name(self) -> &'static str {
match self {
Self::Malformed => "malformed",
Self::ScopeTooLarge => "scope_too_large",
Self::Signature => "signature",
Self::Expired => "expired",
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct PassVerifier {
key: VerifyingKey,
skew: u64,
require_verified_worker: bool,
}
impl PassVerifier {
pub(crate) fn from_auth(auth: &crate::config::AuthConfig) -> Result<Option<Self>, ServerError> {
auth.pass
.as_ref()
.map(|pass| {
Self::from_config(pass).map(|verifier| Self {
require_verified_worker: auth.require_verified_worker,
..verifier
})
})
.transpose()
}
#[cfg(test)]
pub(crate) const fn requiring_verified_worker(self) -> Self {
Self {
require_verified_worker: true,
..self
}
}
pub(crate) const fn requires_verified_worker(&self) -> bool {
self.require_verified_worker
}
pub(crate) fn from_config(config: &PassConfig) -> Result<Self, ServerError> {
let key = hex::decode(&config.registry_verifying_key).map_err(|error| {
ServerError::ConfigValidation {
message: format!(
"auth.pass.registry_verifying_key: expected 64 hexadecimal digits: {error}"
),
}
})?;
Self::new(&key, config.maximum_clock_skew_seconds).map_err(|_| {
ServerError::ConfigValidation {
message: "auth.pass.registry_verifying_key: expected a valid 32-byte Ed25519 verifying key".to_owned(),
}
})
}
pub(crate) fn new(key: &[u8], skew: u64) -> Result<Self, PassCheckError> {
let key: [u8; 32] = key.try_into().map_err(|_| PassCheckError::Malformed)?;
let key = VerifyingKey::from_bytes(&key).map_err(|_| PassCheckError::Malformed)?;
Ok(Self {
key,
skew,
require_verified_worker: false,
})
}
pub(crate) fn verify(&self, bytes: &[u8]) -> Result<VerifiedPass, PassCheckError> {
let principal = WirePassV1::verify(bytes, &self.key, self.skew, SystemTime::now())?;
let holder = VerifiedHolder {
participant: principal.participant.clone(),
live: principal.live.clone(),
source: HolderSource::RegistryPass,
};
Ok(VerifiedPass { principal, holder })
}
}
impl WirePassV1 {
pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, PassCheckError> {
if self.issued_at > self.expires_at || self.conversations.windows(2).any(|p| p[0] >= p[1]) {
return Err(PassCheckError::Malformed);
}
let mut out = vec![PASS_VERSION_V1];
put_bytes(&mut out, &self.participant)?;
out.extend_from_slice(&self.public_key);
put_len(&mut out, self.conversations.len())?;
for id in &self.conversations {
out.extend_from_slice(&id.to_be_bytes());
}
put_bytes(&mut out, self.live.as_bytes())?;
out.push(u8::from(self.may_enroll));
out.extend_from_slice(&self.issued_at.to_be_bytes());
out.extend_from_slice(&self.expires_at.to_be_bytes());
Ok(out)
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, PassCheckError> {
let mut out = self.canonical_unsigned_bytes()?;
out.extend_from_slice(&self.signature);
Ok(out)
}
pub fn parse(bytes: &[u8]) -> Result<Self, PassCheckError> {
let unsigned_len = bytes
.len()
.checked_sub(SIGNATURE_LEN)
.ok_or(PassCheckError::Malformed)?;
let (unsigned, sig) = bytes.split_at(unsigned_len);
let mut c = Cursor(unsigned);
if c.u8()? != PASS_VERSION_V1 {
return Err(PassCheckError::Malformed);
}
let participant = c.bytes()?.to_vec();
let public_key = c.array()?;
let count = c.len()?;
if count > MAX_PASS_CONVERSATIONS {
return Err(PassCheckError::ScopeTooLarge);
}
let mut conversations = Vec::with_capacity(count);
for _ in 0..count {
conversations.push(c.u64()?);
}
let live = std::str::from_utf8(c.bytes()?)
.map_err(|_| PassCheckError::Malformed)?
.to_owned();
let may_enroll = match c.u8()? {
0 => false,
1 => true,
_ => return Err(PassCheckError::Malformed),
};
let issued_at = c.u64()?;
let expires_at = c.u64()?;
if !c.0.is_empty()
|| issued_at > expires_at
|| conversations.windows(2).any(|p| p[0] >= p[1])
{
return Err(PassCheckError::Malformed);
}
Ok(Self {
participant,
public_key,
conversations,
live,
may_enroll,
issued_at,
expires_at,
signature: sig.try_into().map_err(|_| PassCheckError::Malformed)?,
})
}
pub fn verify(
bytes: &[u8],
key: &VerifyingKey,
skew: u64,
now: SystemTime,
) -> Result<PassPrincipal, PassCheckError> {
let pass = Self::parse(bytes)?;
key.verify_strict(
&pass.canonical_unsigned_bytes()?,
&Signature::from_bytes(&pass.signature),
)
.map_err(|_| PassCheckError::Signature)?;
let now = now
.duration_since(UNIX_EPOCH)
.map_err(|_| PassCheckError::Expired)?
.as_secs();
if now < pass.issued_at.saturating_sub(skew) || now > pass.expires_at.saturating_add(skew) {
return Err(PassCheckError::Expired);
}
Ok(PassPrincipal {
participant: pass.participant,
public_key: pass.public_key,
conversations: pass.conversations.into_iter().collect(),
live: pass.live,
may_enroll: pass.may_enroll,
})
}
}
fn put_len(out: &mut Vec<u8>, len: usize) -> Result<(), PassCheckError> {
out.extend_from_slice(
&u32::try_from(len)
.map_err(|_| PassCheckError::Malformed)?
.to_be_bytes(),
);
Ok(())
}
fn put_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), PassCheckError> {
put_len(out, bytes.len())?;
out.extend_from_slice(bytes);
Ok(())
}
struct Cursor<'a>(&'a [u8]);
impl<'a> Cursor<'a> {
const fn take(&mut self, n: usize) -> Result<&'a [u8], PassCheckError> {
if self.0.len() < n {
return Err(PassCheckError::Malformed);
}
let (a, b) = self.0.split_at(n);
self.0 = b;
Ok(a)
}
fn u8(&mut self) -> Result<u8, PassCheckError> {
Ok(self.take(1)?[0])
}
fn len(&mut self) -> Result<usize, PassCheckError> {
let a: [u8; 4] = self
.take(4)?
.try_into()
.map_err(|_| PassCheckError::Malformed)?;
usize::try_from(u32::from_be_bytes(a)).map_err(|_| PassCheckError::Malformed)
}
fn u64(&mut self) -> Result<u64, PassCheckError> {
Ok(u64::from_be_bytes(
self.take(8)?
.try_into()
.map_err(|_| PassCheckError::Malformed)?,
))
}
fn bytes(&mut self) -> Result<&'a [u8], PassCheckError> {
let n = self.len()?;
self.take(n)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N], PassCheckError> {
self.take(N)?
.try_into()
.map_err(|_| PassCheckError::Malformed)
}
}
#[cfg(test)]
mod debug_tests;
#[cfg(test)]
mod tests {
use super::*;
fn pass_declaring(count: u32) -> Vec<u8> {
let mut bytes = vec![PASS_VERSION_V1];
bytes.extend_from_slice(&0_u32.to_be_bytes());
bytes.extend_from_slice(&[0_u8; 32]);
bytes.extend_from_slice(&count.to_be_bytes());
bytes.extend_from_slice(&[0_u8; SIGNATURE_LEN]);
bytes
}
#[test]
fn a_pass_declaring_u32_max_conversations_is_refused_by_name_before_allocation() {
assert_eq!(
WirePassV1::parse(&pass_declaring(u32::MAX)),
Err(PassCheckError::ScopeTooLarge)
);
assert_eq!(
PassCheckError::ScopeTooLarge.check_name(),
"scope_too_large"
);
}
#[test]
fn a_pass_declaring_one_past_the_bound_is_refused_and_the_bound_itself_is_not()
-> Result<(), Box<dyn std::error::Error>> {
let at_bound = u32::try_from(MAX_PASS_CONVERSATIONS)?;
assert_eq!(
WirePassV1::parse(&pass_declaring(at_bound + 1)),
Err(PassCheckError::ScopeTooLarge)
);
assert_eq!(
WirePassV1::parse(&pass_declaring(at_bound)),
Err(PassCheckError::Malformed)
);
Ok(())
}
#[test]
fn shared_version_one_vector_is_byte_exact_and_verifies()
-> Result<(), Box<dyn std::error::Error>> {
let vector: serde_json::Value =
serde_json::from_str(include_str!("../test-vectors/wire-pass-v1.json"))?;
let unsigned = hex::decode(
vector["unsigned_pass_hex"]
.as_str()
.ok_or("unsigned bytes")?,
)?;
let signature: [u8; 64] =
hex::decode(vector["signature_hex"].as_str().ok_or("signature")?)?
.try_into()
.map_err(|_| "signature length")?;
let bytes = hex::decode(vector["pass_hex"].as_str().ok_or("pass bytes")?)?;
let registry_key: [u8; 32] = hex::decode(
vector["registry_verifying_key_hex"]
.as_str()
.ok_or("verifying key")?,
)?
.try_into()
.map_err(|_| "key length")?;
let pass = WirePassV1::parse(&bytes).map_err(|error| format!("parse failed: {error:?}"))?;
assert_eq!(
pass.canonical_unsigned_bytes()
.map_err(|error| format!("encode failed: {error:?}"))?,
unsigned
);
assert_eq!(pass.signature, signature);
let key = VerifyingKey::from_bytes(®istry_key)?;
let principal = WirePassV1::verify(
&bytes,
&key,
0,
UNIX_EPOCH + std::time::Duration::from_secs(pass.issued_at),
)
.map_err(|error| format!("verify failed: {error:?}"))?;
assert_eq!(principal.participant, b"participant-42");
assert_eq!(
principal.conversations.into_iter().collect::<Vec<_>>(),
vec![7, 42, 9001]
);
assert_eq!(principal.live, "workspace/acme/");
assert!(principal.may_enroll);
Ok(())
}
}