arche 4.13.0

An opinionated backend foundation for Axum applications, providing batteries-included integrations for cloud services, databases, authentication, middleware, and logging.
Documentation
use std::future::Future;

use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rsa::RsaPrivateKey;
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::traits::PublicKeyParts;

use crate::error::AppError;

pub trait TokenSigner: Send + Sync + 'static {
    fn kid(&self) -> &str;

    fn sign(&self, message: &[u8]) -> impl Future<Output = Result<Vec<u8>, AppError>> + Send;

    fn jwks(&self) -> serde_json::Value;
}

pub fn rsa_public_jwk(kid: &str, public_key_pem: &str) -> Result<serde_json::Value, AppError> {
    use rsa::RsaPublicKey;
    use rsa::pkcs1::DecodeRsaPublicKey;
    use rsa::pkcs8::DecodePublicKey;

    let key = RsaPublicKey::from_public_key_pem(public_key_pem)
        .or_else(|_| RsaPublicKey::from_pkcs1_pem(public_key_pem))
        .map_err(|e| AppError::internal_error(format!("Invalid RSA public key PEM: {e}"), None))?;
    Ok(jwk_from_parts(
        kid,
        &key.n().to_bytes_be(),
        &key.e().to_bytes_be(),
    ))
}

fn jwk_from_parts(kid: &str, n: &[u8], e: &[u8]) -> serde_json::Value {
    serde_json::json!({
        "kty": "RSA",
        "alg": "RS256",
        "use": "sig",
        "kid": kid,
        "n": URL_SAFE_NO_PAD.encode(n),
        "e": URL_SAFE_NO_PAD.encode(e),
    })
}

#[derive(Clone)]
pub struct SigningKey {
    pub kid: String,
    encoding: jsonwebtoken::EncodingKey,
    jwk: serde_json::Value,
}

impl std::fmt::Debug for SigningKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SigningKey")
            .field("kid", &self.kid)
            .field("key", &"<redacted>")
            .finish()
    }
}

impl SigningKey {
    pub fn from_pem(kid: impl Into<String>, pem: &str) -> Result<Self, AppError> {
        let kid = kid.into();
        if kid.is_empty() {
            return Err(AppError::internal_error(
                "SigningKey kid must not be empty".into(),
                None,
            ));
        }

        let private = RsaPrivateKey::from_pkcs8_pem(pem)
            .or_else(|_| RsaPrivateKey::from_pkcs1_pem(pem))
            .map_err(|e| {
                AppError::internal_error(format!("Invalid RSA private key PEM: {e}"), None)
            })?;
        if private.n().bits() < 2048 {
            return Err(AppError::internal_error(
                format!(
                    "RSA signing key must be at least 2048 bits, got {}",
                    private.n().bits()
                ),
                None,
            ));
        }

        let encoding = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| {
            AppError::internal_error(format!("Invalid RSA private key PEM: {e}"), None)
        })?;

        let jwk = jwk_from_parts(&kid, &private.n().to_bytes_be(), &private.e().to_bytes_be());
        Ok(Self { kid, encoding, jwk })
    }
}

impl TokenSigner for SigningKey {
    fn kid(&self) -> &str {
        &self.kid
    }

    async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, AppError> {
        let b64 =
            jsonwebtoken::crypto::sign(message, &self.encoding, jsonwebtoken::Algorithm::RS256)
                .map_err(|e| AppError::internal_error(format!("RSA signing failed: {e}"), None))?;
        URL_SAFE_NO_PAD
            .decode(b64)
            .map_err(|e| AppError::internal_error(format!("RSA signature decode: {e}"), None))
    }

    fn jwks(&self) -> serde_json::Value {
        serde_json::json!({ "keys": [self.jwk] })
    }
}