Skip to main content

kcode_k1_http/
lib.rs

1use async_trait::async_trait;
2use axum::Router;
3use axum::body::Body;
4use axum::extract::{Request, State};
5use axum::http::header::{ALLOW, CACHE_CONTROL, CONTENT_TYPE, HOST};
6use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
7use axum::middleware::{self, Next};
8use axum::response::Response;
9use axum::routing::MethodRouter;
10use kcode_k1_http_replay::{ReplayError, ReplayWindow};
11use kcode_k1_http_request::{Envelope, VerifyError, registration_public_key};
12use std::fmt::{self, Display, Formatter};
13use std::sync::Arc;
14use tower_http::cors::{Any, CorsLayer};
15
16pub use kcode_k1_http_request::CanonicalUsername;
17
18pub struct Config {
19    pub server_id: String,
20    pub public_origin: String,
21    pub max_body_bytes: usize,
22}
23
24#[derive(Clone, Copy, Eq, PartialEq)]
25pub struct Identity {
26    user_id: [u8; 12],
27    public_key: [u8; 32],
28}
29
30impl Identity {
31    pub fn new(user_id: [u8; 12], public_key: [u8; 32]) -> Self {
32        Self {
33            user_id,
34            public_key,
35        }
36    }
37
38    pub fn user_id(&self) -> &[u8; 12] {
39        &self.user_id
40    }
41
42    pub fn public_key(&self) -> &[u8; 32] {
43        &self.public_key
44    }
45}
46
47#[derive(Clone)]
48pub struct Principal {
49    user_id: [u8; 12],
50    username: CanonicalUsername,
51}
52
53impl Principal {
54    pub fn user_id(&self) -> &[u8; 12] {
55        &self.user_id
56    }
57
58    pub fn username(&self) -> &CanonicalUsername {
59        &self.username
60    }
61}
62
63#[derive(Clone)]
64pub struct RegistrationPrincipal {
65    username: CanonicalUsername,
66    public_key: [u8; 32],
67}
68
69impl RegistrationPrincipal {
70    pub fn username(&self) -> &CanonicalUsername {
71        &self.username
72    }
73
74    pub fn public_key(&self) -> &[u8; 32] {
75        &self.public_key
76    }
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum IdentityError {
81    Unavailable,
82}
83
84impl Display for IdentityError {
85    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
86        formatter.write_str("identity provider unavailable")
87    }
88}
89
90impl std::error::Error for IdentityError {}
91
92#[async_trait]
93pub trait IdentityProvider: Send + Sync + 'static {
94    async fn lookup(&self, username: &CanonicalUsername)
95    -> Result<Option<Identity>, IdentityError>;
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum ConfigError {
100    EmptyServerId,
101    InvalidPublicOrigin,
102    InvalidBodyLimit,
103}
104
105impl Display for ConfigError {
106    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
107        formatter.write_str(match self {
108            Self::EmptyServerId => "server ID is empty",
109            Self::InvalidPublicOrigin => "public origin is invalid",
110            Self::InvalidBodyLimit => "body limit is invalid",
111        })
112    }
113}
114
115impl std::error::Error for ConfigError {}
116
117pub struct K1Http {
118    state: Arc<AppState>,
119}
120
121struct AppState {
122    server_id: String,
123    public_origin: String,
124    authority: String,
125    max_body_bytes: usize,
126    replay: ReplayWindow,
127    identities: Arc<dyn IdentityProvider>,
128}
129
130impl K1Http {
131    pub fn new(
132        config: Config,
133        replay: kcode_k1_http_replay::ReplayWindow,
134        identities: Arc<dyn IdentityProvider>,
135    ) -> Result<Self, ConfigError> {
136        if config.server_id.is_empty() {
137            return Err(ConfigError::EmptyServerId);
138        }
139        if config.max_body_bytes == 0 {
140            return Err(ConfigError::InvalidBodyLimit);
141        }
142        let authority =
143            origin_authority(&config.public_origin).ok_or(ConfigError::InvalidPublicOrigin)?;
144        Ok(Self {
145            state: Arc::new(AppState {
146                server_id: config.server_id,
147                public_origin: config.public_origin,
148                authority,
149                max_body_bytes: config.max_body_bytes,
150                replay,
151                identities,
152            }),
153        })
154    }
155
156    pub fn router(
157        &self,
158        registration: MethodRouter,
159        terms: MethodRouter,
160        authenticated: Router,
161    ) -> Router {
162        let authenticated = authenticated.layer(middleware::from_fn_with_state(
163            self.state.clone(),
164            authenticate,
165        ));
166        let registration = registration.layer(middleware::from_fn_with_state(
167            self.state.clone(),
168            authenticate_registration,
169        ));
170        Router::new()
171            .nest("/api", authenticated)
172            .route("/api/register", registration)
173            .route("/api/terms", terms)
174            .layer(middleware::from_fn(terms_method_guard))
175            .layer(cors())
176            .layer(middleware::from_fn_with_state(self.state.clone(), api_gate))
177    }
178}
179
180fn origin_authority(value: &str) -> Option<String> {
181    let uri: Uri = value.parse().ok()?;
182    let scheme = uri.scheme_str()?;
183    if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
184        return None;
185    }
186    let authority = uri.authority()?.as_str();
187    if authority.is_empty() || authority.contains('@') || uri.query().is_some() {
188        return None;
189    }
190    if !matches!(uri.path(), "" | "/") {
191        return None;
192    }
193    Some(authority.to_owned())
194}
195
196fn cors() -> CorsLayer {
197    CorsLayer::new()
198        .allow_origin(Any)
199        .allow_methods([
200            Method::GET,
201            Method::POST,
202            Method::PUT,
203            Method::PATCH,
204            Method::DELETE,
205            Method::OPTIONS,
206            Method::HEAD,
207        ])
208        .allow_headers([
209            CONTENT_TYPE,
210            header("k1-username"),
211            header("k1-epoch"),
212            header("k1-nonce"),
213            header("k1-body-sha256"),
214            header("k1-signature"),
215            header("k1-public-key"),
216        ])
217        .expose_headers([header("k1-epoch")])
218}
219
220fn header(value: &'static str) -> HeaderName {
221    HeaderName::from_static(value)
222}
223
224fn set(headers: &mut HeaderMap, name: HeaderName, value: &'static str) {
225    headers.insert(name, HeaderValue::from_static(value));
226}
227
228async fn api_gate(State(state): State<Arc<AppState>>, request: Request, next: Next) -> Response {
229    let authority_ok = single(request.headers(), HOST.as_str())
230        .is_ok_and(|value| value.as_bytes() == state.authority.as_bytes());
231    if !authority_ok {
232        return finish(
233            &state,
234            error(StatusCode::MISDIRECTED_REQUEST, "invalid_request_authority"),
235        )
236        .await;
237    }
238    if state.replay.current_epoch().await.is_err() {
239        return decorate(
240            error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
241            None,
242        );
243    }
244    finish(&state, next.run(request).await).await
245}
246
247async fn terms_method_guard(request: Request, next: Next) -> Response {
248    if request.uri().path() == "/api/terms"
249        && request.method() != Method::GET
250        && request.method() != Method::HEAD
251    {
252        return method_not_allowed("GET, HEAD");
253    }
254    next.run(request).await
255}
256
257async fn finish(state: &AppState, response: Response) -> Response {
258    match state.replay.current_epoch().await {
259        Ok(epoch) => decorate(response, Some(epoch)),
260        Err(_) => decorate(
261            error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
262            None,
263        ),
264    }
265}
266
267fn decorate(mut response: Response, epoch: Option<u64>) -> Response {
268    let headers = response.headers_mut();
269    set(headers, CACHE_CONTROL, "no-store");
270    set(headers, header("x-content-type-options"), "nosniff");
271    set(headers, header("access-control-allow-origin"), "*");
272    set(headers, header("access-control-expose-headers"), "k1-epoch");
273    if let Some(epoch) = epoch
274        && let Ok(value) = HeaderValue::from_str(&epoch.to_string())
275    {
276        headers.insert(header("k1-epoch"), value);
277    }
278    response
279}
280
281fn single<'a>(headers: &'a HeaderMap, name: &str) -> Result<&'a HeaderValue, ()> {
282    let mut values = headers.get_all(name).iter();
283    let value = values.next().ok_or(())?;
284    if values.next().is_some() {
285        return Err(());
286    }
287    Ok(value)
288}
289
290async fn authenticate(
291    State(state): State<Arc<AppState>>,
292    request: Request,
293    next: Next,
294) -> Response {
295    proceed(authenticated_request(&state, request).await, next).await
296}
297
298async fn authenticate_registration(
299    State(state): State<Arc<AppState>>,
300    request: Request,
301    next: Next,
302) -> Response {
303    if request.method() != Method::POST {
304        return method_not_allowed("POST");
305    }
306    proceed(registration_request(&state, request).await, next).await
307}
308
309async fn proceed(result: Result<Request, Response>, next: Next) -> Response {
310    match result {
311        Ok(request) => next.run(request).await,
312        Err(response) => response,
313    }
314}
315
316async fn authenticated_request(state: &AppState, request: Request) -> Result<Request, Response> {
317    let envelope = Envelope::parse(request.headers())
318        .map_err(|_| error(StatusCode::BAD_REQUEST, "malformed_authentication_envelope"))?;
319    validate_epoch(state, envelope.epoch()).await?;
320    let identity = match state.identities.lookup(envelope.username()).await {
321        Ok(Some(identity)) => identity,
322        Ok(None) => return Err(error(StatusCode::UNAUTHORIZED, "authentication_failed")),
323        Err(IdentityError::Unavailable) => {
324            return Err(error(
325                StatusCode::SERVICE_UNAVAILABLE,
326                "identity_provider_unavailable",
327            ));
328        }
329    };
330    let mut request = verified(state, &envelope, request, identity.public_key()).await?;
331    let mut replay_id = [0_u8; 32];
332    replay_id[..12].copy_from_slice(identity.user_id());
333    state
334        .replay
335        .admit(&replay_id, envelope.epoch(), *envelope.nonce())
336        .await
337        .map_err(replay_error)?;
338    request.extensions_mut().insert(Principal {
339        user_id: *identity.user_id(),
340        username: envelope.username().clone(),
341    });
342    Ok(request)
343}
344
345async fn registration_request(state: &AppState, request: Request) -> Result<Request, Response> {
346    let envelope = Envelope::parse(request.headers())
347        .map_err(|_| error(StatusCode::BAD_REQUEST, "malformed_authentication_envelope"))?;
348    if single(request.headers(), "k1-public-key").is_err() {
349        return Err(error(
350            StatusCode::BAD_REQUEST,
351            "malformed_authentication_envelope",
352        ));
353    }
354    let candidate = registration_public_key(request.headers())
355        .map_err(|_| error(StatusCode::UNAUTHORIZED, "authentication_failed"))?;
356    validate_epoch(state, envelope.epoch()).await?;
357    let mut request = verified(state, &envelope, request, &candidate).await?;
358    request.extensions_mut().insert(RegistrationPrincipal {
359        username: envelope.username().clone(),
360        public_key: candidate,
361    });
362    Ok(request)
363}
364
365async fn verified(
366    state: &AppState,
367    envelope: &Envelope,
368    request: Request,
369    public_key: &[u8; 32],
370) -> Result<Request, Response> {
371    envelope
372        .verify(
373            request,
374            &state.server_id,
375            &state.public_origin,
376            public_key,
377            state.max_body_bytes,
378        )
379        .await
380        .map_err(verify_error)
381}
382
383async fn validate_epoch(state: &AppState, epoch: u64) -> Result<(), Response> {
384    state
385        .replay
386        .validate_epoch(epoch)
387        .await
388        .map(|_| ())
389        .map_err(replay_error)
390}
391
392fn replay_error(cause: ReplayError) -> Response {
393    match cause {
394        ReplayError::EpochOutsideWindow { .. } => error(StatusCode::UNAUTHORIZED, "stale_epoch"),
395        ReplayError::Replay { .. } => error(StatusCode::CONFLICT, "replay"),
396        ReplayError::CapacityExceeded { .. } => {
397            error(StatusCode::TOO_MANY_REQUESTS, "nonce_capacity")
398        }
399        _ => error(StatusCode::SERVICE_UNAVAILABLE, "epoch_unavailable"),
400    }
401}
402
403fn verify_error(cause: VerifyError) -> Response {
404    match cause {
405        VerifyError::AuthenticationFailed => {
406            error(StatusCode::UNAUTHORIZED, "authentication_failed")
407        }
408        VerifyError::BodyDigestMismatch => error(StatusCode::BAD_REQUEST, "body_digest_mismatch"),
409        VerifyError::BodyTooLarge => error(StatusCode::PAYLOAD_TOO_LARGE, "body_too_large"),
410    }
411}
412
413fn method_not_allowed(allow: &'static str) -> Response {
414    let mut response = Response::new(Body::empty());
415    *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
416    set(response.headers_mut(), ALLOW, allow);
417    response
418}
419
420fn error(status: StatusCode, code: &'static str) -> Response {
421    let mut response = Response::new(Body::from(format!("{{\"error\":\"{code}\"}}")));
422    *response.status_mut() = status;
423    set(response.headers_mut(), CONTENT_TYPE, "application/json");
424    response
425}