From 65c27038852cb961af17efd8257ba3a6b5f131d4 Mon Sep 17 00:00:00 2001 From: shimun Date: Sun, 18 Jun 2023 11:23:26 +0200 Subject: [PATCH 1/2] feat(extract): wip --- src/api.rs | 41 ++++++++++++++---- src/api/extract.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/src/api.rs b/src/api.rs index 517ac3a..6e6abe3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,6 +10,7 @@ use crate::certs::{load_cert_by_id, read_certs, read_pubkey, store_cert}; use crate::env_key; use anyhow::Context; use axum::body; +use axum::extract::rejection::QueryRejection; use axum::extract::{Query, State}; use axum::{http::StatusCode, response::IntoResponse, Json, Router}; @@ -29,7 +30,7 @@ use tower::ServiceBuilder; use tower_http::{trace::TraceLayer, ServiceBuilderExt}; use tracing::{debug, info, trace}; -use self::extract::{CertificateBody, SignatureBody}; +use self::extract::{AsJWTVerifier, CertificateBody, JWTAuthenticated, JWTString, SignatureBody}; #[derive(Parser)] pub struct ApiArgs { @@ -86,6 +87,13 @@ struct ApiState { jwt_key: Hs256Key, } +impl AsJWTVerifier for ApiState { + type Algo = Hs256; + fn as_secret(&self) -> &::VerifyingKey { + &self.jwt_key + } +} + impl ApiState { async fn new( cert_dir: impl AsRef, @@ -177,6 +185,12 @@ pub enum ApiError { AuthenticationRequired(String), #[error("invalid ssh signature")] InvalidSignature, + #[error("invalid jwt")] + JWTVerify(#[from] jwt_compact::ValidationError), + #[error("invalid jwt")] + JWTParse(#[from] jwt_compact::ParseError), + #[error("{0}")] + Query(#[from] QueryRejection), } type ApiResult = Result; @@ -221,7 +235,7 @@ async fn list_certs( )) } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "aud", rename = "get")] struct AuthClaims { identifier: String, @@ -323,22 +337,28 @@ struct PostCertsQuery { challenge: String, } +impl Into for Query { + fn into(self) -> JWTString { + self.0.challenge.into() + } +} + /// POST with signed challenge async fn post_certs_identifier( PostCertInfo { identifier }: PostCertInfo, State(ApiState { certs, jwt_key, .. }): State, + JWTAuthenticated { + data: AuthClaims { + identifier: authenticated_identifier, + }, + .. + }: JWTAuthenticated, ApiError>, Query(PostCertsQuery { challenge }): Query, SignatureBody(sig): SignatureBody, ) -> ApiResult { let certs = certs.lock().await; let cert = certs.get(&identifier).ok_or(ApiError::InvalidSignature)?; - let token: Token = Hs256 - .validate_integrity( - &UntrustedToken::new(&challenge).context("jwt parse")?, - &jwt_key, - ) - .map_err(|_| ApiError::InvalidSignature)?; - if token.claims().custom.identifier != identifier { + if authenticated_identifier != identifier { return Err(ApiError::InvalidSignature); } let pubkey: PublicKey = cert.public_key().clone().into(); @@ -537,6 +557,9 @@ mod tests { identifier: "test_cert".into(), }, State(state.clone()), + JWTAuthenticated::from(AuthClaims { + identifier: "test_cert".into(), + }), Query(PostCertsQuery { challenge }), SignatureBody(sig), ) diff --git a/src/api/extract.rs b/src/api/extract.rs index 4b9ef79..b8b138b 100644 --- a/src/api/extract.rs +++ b/src/api/extract.rs @@ -1,10 +1,22 @@ +use super::ApiError; use anyhow::Context; use axum::{ - async_trait, body::BoxBody, extract::FromRequest, http::Request, response::IntoResponse, + async_trait, + body::BoxBody, + extract::{FromRequest, FromRequestParts}, + http::Request, + response::IntoResponse, }; +use jwt_compact::{ + alg::{SigningKey, VerifyingKey}, + AlgorithmSignature, ParseError, Token, UntrustedToken, ValidationError, +}; +use jwt_compact::{Algorithm, AlgorithmExt}; +use serde::{de::DeserializeOwned, Serialize}; use ssh_key::{Certificate, SshSig}; +use std::marker::PhantomData; +use std::{fmt::Debug, ops::Deref}; use tracing::trace; -use super::ApiError; #[derive(Debug, Clone)] pub struct CertificateBody(pub Certificate); @@ -49,3 +61,89 @@ where Ok(Self(sig)) } } + +pub trait AsJWTVerifier: Send + Sync { + type Algo: Algorithm + Default; + fn as_secret(&self) -> &::VerifyingKey; +} + +pub struct JWTString(String); + +impl From for JWTString { + fn from(s: String) -> Self { + Self(s) + } +} + +#[derive(Debug)] +pub struct JWTAuthenticated { + pub data: T, + _marker: PhantomData<(Q, S, E)>, +} + +impl From for JWTAuthenticated { + fn from(data: T) -> Self { + Self { + data, + _marker: Default::default(), + } + } +} + +impl Deref for JWTAuthenticated { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl< + T: Serialize + DeserializeOwned + Clone + Debug, + S: AsJWTVerifier, + Q: FromRequestParts + Debug + Into, + E: From<>::Rejection> + + From + + From + + Debug + + Send + + Sync, + > JWTAuthenticated +{ + pub fn new(data: T) -> Self { + Self { + data, + _marker: Default::default(), + } + } +} + +#[async_trait] +impl< + T: Serialize + DeserializeOwned + Clone + Debug, + S: AsJWTVerifier, + Q: FromRequestParts + Debug + Into, + E: From<>::Rejection> + + From + + From + + Debug + + Send + + Sync + + IntoResponse, + > FromRequestParts for JWTAuthenticated +{ + type Rejection = E; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result { + let JWTString(token) = Q::from_request_parts(parts, state).await?.into(); + let token = UntrustedToken::new(&token)?; + let verified: Token = + ::default().validate_integrity(&token, &state.as_secret())?; + Ok(Self { + data: verified.claims().custom.clone(), + _marker: Default::default(), + }) + } +} -- 2.49.0 From e4c9d608147cec310589d63e76d3328184429345 Mon Sep 17 00:00:00 2001 From: shimun Date: Mon, 19 Jun 2023 19:22:21 +0200 Subject: [PATCH 2/2] fear(ci): added woodpecker --- .woodpecker.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..8785d9a --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,5 @@ +pipeline: + test: + image: rust + commands: + - cargo test \ No newline at end of file -- 2.49.0