From 82ab7f7d2c710aaed04de8a0c38e4160ed019bf8 Mon Sep 17 00:00:00 2001 From: oxalica Date: Thu, 19 Sep 2024 02:34:21 -0400 Subject: [PATCH] refactor(types): move `UserIdentityDesc` verification into types crate --- blah-types/src/identity.rs | 60 ++++++++++++++++++++++++++++++++++++++ blahd/src/register.rs | 59 +++++-------------------------------- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/blah-types/src/identity.rs b/blah-types/src/identity.rs index cb0a1fc..fea3aaf 100644 --- a/blah-types/src/identity.rs +++ b/blah-types/src/identity.rs @@ -2,6 +2,7 @@ use core::fmt; use std::ops; use std::str::FromStr; +use ed25519_dalek::SignatureError; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::{Host, Position, Url}; @@ -20,8 +21,67 @@ pub struct UserIdentityDesc { pub profile: Signed, } +#[derive(Debug, Error)] +#[error(transparent)] +pub struct VerfiyError(#[from] VerifyErrorImpl); + +#[derive(Debug, Error)] +enum VerifyErrorImpl { + #[error("profile id_key mismatch")] + ProfileIdKeyMismatch, + #[error("act_key[{0}] not signed by id_key")] + ActKeySigner(usize), + #[error("invalid act_key[{0}] signature: {1}")] + ActKeySignature(usize, SignatureError), + #[error("profile is not signed by any valid act_key")] + ProfileSigner, + #[error("invalid profile signature: {0}")] + ProfileSignature(SignatureError), + #[error("id_url is not in the valid list")] + MissingIdUrl, +} + impl UserIdentityDesc { pub const WELL_KNOWN_PATH: &str = "/.well-known/blah/identity.json"; + + /// Validate signatures of the identity description at given time. + pub fn verify(&self, id_url: Option<&IdUrl>, now_timestamp: u64) -> Result<(), VerfiyError> { + if self.id_key != self.profile.signee.user.id_key { + return Err(VerifyErrorImpl::ProfileIdKeyMismatch.into()); + } + + let profile_signing_key = &self.profile.signee.user.act_key; + let mut profile_signed = false; + + for (i, signed_kdesc) in self.act_keys.iter().enumerate() { + let kdesc = &signed_kdesc.signee.payload; + // act_key itself is signed by id_key, so both are id_key here. + if signed_kdesc.signee.user.id_key != self.id_key + || signed_kdesc.signee.user.act_key != self.id_key + { + return Err(VerifyErrorImpl::ActKeySigner(i).into()); + } + signed_kdesc + .verify() + .map_err(|err| VerifyErrorImpl::ActKeySignature(i, err))?; + if now_timestamp < kdesc.expire_time && *profile_signing_key == kdesc.act_key { + profile_signed = true; + } + } + + if !profile_signed { + return Err(VerifyErrorImpl::ProfileSigner.into()); + } + self.profile + .verify() + .map_err(VerifyErrorImpl::ProfileSignature)?; + if let Some(id_url) = id_url { + if !self.profile.signee.payload.id_urls.contains(id_url) { + return Err(VerifyErrorImpl::MissingIdUrl.into()); + } + } + Ok(()) + } } // TODO: JWS or alike? diff --git a/blahd/src/register.rs b/blahd/src/register.rs index 05dfd44..24c7aa3 100644 --- a/blahd/src/register.rs +++ b/blahd/src/register.rs @@ -1,12 +1,10 @@ use std::num::NonZero; use std::time::{Duration, Instant}; -use anyhow::{anyhow, ensure, Context}; +use anyhow::{anyhow, ensure}; use axum::http::{HeaderMap, HeaderName, StatusCode}; use blah_types::identity::{IdUrl, UserIdentityDesc}; -use blah_types::{ - get_timestamp, PubKey, Signed, UserRegisterPayload, X_BLAH_DIFFICULTY, X_BLAH_NONCE, -}; +use blah_types::{get_timestamp, Signed, UserRegisterPayload, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; use http_body_util::BodyExt; use parking_lot::Mutex; use rand::rngs::OsRng; @@ -246,7 +244,11 @@ pub async fn user_register( } }; - if let Err(err) = validate_id_desc(®.id_url, ®.id_key, &id_desc, fetch_time) { + if let Err(err) = (|| { + ensure!(reg.id_key == id_desc.id_key, "id_key mismatch"); + id_desc.verify(Some(®.id_url), fetch_time)?; + Ok(()) + })() { return Err(error_response!( StatusCode::UNAUTHORIZED, "invalid_id_description", @@ -314,53 +316,6 @@ pub async fn user_register( Ok(StatusCode::NO_CONTENT) } -fn validate_id_desc( - id_url: &IdUrl, - id_key: &PubKey, - id_desc: &UserIdentityDesc, - now: u64, -) -> anyhow::Result<()> { - ensure!(*id_key == id_desc.id_key, "id_key mismatch"); - - ensure!( - *id_key == id_desc.profile.signee.user.id_key, - "profile id_key mismatch", - ); - let profile_signing_key = &id_desc.profile.signee.user.act_key; - let mut profile_signed = false; - - for (i, signed_kdesc) in id_desc.act_keys.iter().enumerate() { - let kdesc = &signed_kdesc.signee.payload; - (|| { - // act_key itself is signed by id_key, so both are id_key here. - ensure!( - signed_kdesc.signee.user.id_key == *id_key - && signed_kdesc.signee.user.act_key == *id_key, - "not signed by id_key", - ); - signed_kdesc - .verify() - .context("signature verification failed")?; - if now < kdesc.expire_time && *profile_signing_key == kdesc.act_key { - profile_signed = true; - } - Ok(()) - })() - .map_err(|err| anyhow!("invalid act_key[{}] {}: {}", i, kdesc.act_key, err))?; - } - - ensure!(profile_signed, "profile is not signed by valid act_keys"); - id_desc - .profile - .verify() - .context("profile signature verification failed")?; - ensure!( - id_desc.profile.signee.payload.id_urls == std::slice::from_ref(id_url), - "id_url list must consists of a single matching id_url", - ); - Ok(()) -} - #[cfg(test)] mod tests { use super::*;