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}