aauth 0.0.2

Rust implementation of the AAuth authorization protocol
Documentation
use std::time::{SystemTime, UNIX_EPOCH};

use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ed25519_dalek::{Signer, SigningKey};
use http::header::{AUTHORIZATION, HeaderName, HeaderValue};
use reqwest::Request;

use crate::error::{AAuthError, Result};
use crate::headers::{build_capabilities_header, build_mission_header};
use crate::jwt::OkpSigningJwk;
use crate::signature::build_signature_base;
use crate::types::{Capability, KeyMaterial, Mission, SignatureKey};

#[derive(Debug, Clone, Default)]
pub struct SigningOptions {
    pub capabilities: Option<Vec<Capability>>,
    pub mission: Option<Mission>,
}

pub fn apply_capability_mission(request: &mut Request, options: &SigningOptions) {
    if let Some(capabilities) = &options.capabilities {
        if !capabilities.is_empty() {
            request.headers_mut().insert(
                HeaderName::from_static("aauth-capabilities"),
                HeaderValue::from_str(&build_capabilities_header(capabilities))
                    .expect("valid capabilities header"),
            );
        }
    }
    if let Some(mission) = &options.mission {
        request.headers_mut().insert(
            HeaderName::from_static("aauth-mission"),
            HeaderValue::from_str(&build_mission_header(mission)).expect("valid mission header"),
        );
    }
}

pub fn apply_opaque_token(request: &mut Request, token: &str) {
    request.headers_mut().insert(
        AUTHORIZATION,
        HeaderValue::from_str(&format!("AAuth {token}")).expect("valid authorization header"),
    );
}

pub fn sign_request(request: &mut Request, material: &KeyMaterial) -> Result<()> {
    let token = match &material.signature_key {
        SignatureKey::Jwt(j) => &j.jwt,
        SignatureKey::JktJwt(j) => &j.jwt,
        SignatureKey::Hwk(_) => {
            return Err(AAuthError::Message(
                "hwk signature key not supported for AAuth requests".into(),
            ));
        }
    };
    let signature_key = format!("sig=jwt;jwt=\"{token}\"");

    request.headers_mut().insert(
        HeaderName::from_static("signature-key"),
        HeaderValue::from_str(&signature_key).map_err(|e| AAuthError::Message(e.to_string()))?,
    );

    let signing_key = signing_key_from_jwk(&material.signing_jwk)?;
    let created = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let signature_input =
        format!("sig=(\"@method\" \"@authority\" \"@path\" \"signature-key\");created={created}");
    let url = request.url();
    let signature_base = build_signature_base(
        request.method().as_str(),
        url.authority(),
        url.path(),
        &signature_key,
        created,
    );
    let signature_bytes = signing_key.sign(signature_base.as_bytes());
    let signature = URL_SAFE_NO_PAD.encode(signature_bytes.to_bytes());

    request.headers_mut().insert(
        HeaderName::from_static("signature-input"),
        HeaderValue::from_str(&signature_input).map_err(|e| AAuthError::Message(e.to_string()))?,
    );
    request.headers_mut().insert(
        HeaderName::from_static("signature"),
        HeaderValue::from_str(&format!("sig=:{signature}:"))
            .map_err(|e| AAuthError::Message(e.to_string()))?,
    );

    Ok(())
}

pub fn sign_request_with_auth_token(
    request: &mut Request,
    material: &KeyMaterial,
    auth_token: &str,
) -> Result<()> {
    let mut auth_material = material.clone();
    auth_material.signature_key = SignatureKey::Jwt(crate::types::SignatureKeyJwt {
        jwt: auth_token.to_string(),
    });
    sign_request(request, &auth_material)
}

fn signing_key_from_jwk(jwk: &OkpSigningJwk) -> Result<SigningKey> {
    let bytes = URL_SAFE_NO_PAD
        .decode(&jwk.d)
        .map_err(|e| AAuthError::Message(e.to_string()))?;
    let key_bytes: [u8; 32] = bytes
        .try_into()
        .map_err(|_| AAuthError::Message("invalid Ed25519 private key length".into()))?;
    Ok(SigningKey::from_bytes(&key_bytes))
}