Skip to main content

kcode_k1_http/
lib.rs

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